/**
 * @typedef {Object} Constants
 */

const Constants = {
  EventType: {
    IDLE: 'idle',
    CHANGED: 'changed',
    RENDERED: 'rendered', // Internal use only
    LOADED: 'loaded',
    UNDO: 'undo',
    REDO: 'redo',
    CLEAR: 'clear',
    ERROR: 'error'
  }
};

/**
 * Default configuration
 * @type {Configuration}
 */
const defaultConfiguration = {
  listenerOptions: {
    capture: false,
    passive: true
  },
  undoRedoMaxStackSize: 50,
  xyFloatPrecision: 4,
  timestampFloatPrecision: 0,
  renderingParams: {
    stroker: 'quadratic',
    minHeight: 100,
    minWidth: 100
  }
};

/**
 * Generate parameters
 * @param {Configuration} configuration Configuration to be used
 * @return {Configuration} Overridden configuration
 */
function overrideDefaultConfiguration(configuration) {
  let currentConfiguration;

  currentConfiguration = Object.assign(
    {},
    defaultConfiguration,
    configuration === undefined ? {} : configuration
  );
  return currentConfiguration
}

/**
 * @typedef {Object} PenStyle
 * @property {String} color=#000000 Color (supported formats rgb() rgba() hsl() hsla() #rgb #rgba #rrggbb #rrggbbaa)
 * @property {String} -pen-width=1 Width of strokes and primitives in mm (no other unit is supported yet)
 * @property {String} -pen-fill-style=none
 * @property {String} -pen-fill-color=#FFFFFF00 Color filled inside the area delimited by strokes and primitives
 */

/**
 * Default style
 * @type {PenStyle}
 */
const defaultPenStyle = undefined;

/**
 * Generate style
 * @param {PenStyle} style Custom style to be applied
 * @return {PenStyle} Overridden style
 */
function overrideDefaultPenStyle(style) {
  const currentStyle = Object.assign(
    {
      type: 'pen'
    },
    defaultPenStyle,
    style === undefined ? {} : style
  );
  return currentStyle
}

/**
 * @typedef {PenStyle} InkTheme
 */

/**
 * @typedef {Object} TextTheme
 * @property {String} font-family=OpenSans Font-family to be used
 * @property {Number} font-size=10 Font-size to be used
 */
/**
 * @typedef {Object} Theme
 * @property {InkTheme} ink General settings
 * @property {MathTheme} .math Math theme
 * @property {GeneratedTheme} .math-solver Theme to be used for generated items
 * @property {TextTheme} .text Text theme
 */

/**
 * Default theme
 * @type {Theme}
 */
const defaultTheme = {
  ink: {
    color: '#000000',
    penWidth: 1
  },
  text: {},
  eraser: {
    color: 'transparent',
    strokeColor: '#000000',
    penWidth: 15,
    lineWidth: 1
  },
  shape: {
    lineWidth: 1,
    color: '#000000'
  }
};
// const parser = new JsonCSS();

/**
 * Generate theme
 * @param {Theme} theme Custom theme to be applied
 * @return {Theme} Overridden theme
 */
function overrideDefaultTheme(theme = {}, newTheme = {}) {
  const currentTheme = Object.assign({}, defaultTheme, theme, newTheme);
  return currentTheme
}

const floatPrecisionArray = [
  1,
  10,
  100,
  1000,
  10000,
  100000,
  1000000,
  10000000,
  100000000,
  1000000000
];

function roundFloat(oneFloat, requestedFloatPrecision) {
  if (requestedFloatPrecision || requestedFloatPrecision === 0) {
    let floatPrecision;
    if (requestedFloatPrecision > 10) {
      floatPrecision = floatPrecisionArray[10];
    } else {
      floatPrecision = floatPrecisionArray[requestedFloatPrecision];
    }
    return Math.round(oneFloat * floatPrecision) / floatPrecision
  }
  return oneFloat
}

function extractPoint(
  event,
  domElement,
  configuration = {
    xyFloatPrecision: 0,
    timestampFloatPrecision: 0
  },
  offsetTop = 0,
  offsetLeft = 0
) {
  let eventRef = event;
  if (eventRef.changedTouches) {
    eventRef = eventRef.changedTouches[0];
  }
  const rect = domElement.getBoundingClientRect();
  return {
    x: roundFloat(
      eventRef.clientX - rect.left - domElement.clientLeft - offsetLeft,
      configuration.xyFloatPrecision
    ),
    y: roundFloat(
      eventRef.clientY - rect.top - domElement.clientTop - offsetTop,
      configuration.xyFloatPrecision
    ),
    t: roundFloat(Date.now(), configuration.timestampFloatPrecision),
    p:
      event.pressure && event.pressure !== 0.5
        ? Math.min(event.pressure * 1.25, 1)
        : 0
  }
}

/**
 * Grab pointerDown, pointerMove and pointerUp events
 * @typedef {Object} Grabber
 * @property {function(element: Element, editor: Editor): GrabberContext} attach Attach events and decide when to call editor pointerDown/Move/Up methods
 * @property {function(element: Element, context: GrabberContext)} detach Detach the grabber
 */

/**
 * Grabber listener
 * @typedef {Object} GrabberListener
 * @property {Array<String>} types Event types to listen
 * @property {function(event: Event)} listener Event listener for these events
 */

/**
 * Grabber context
 * @typedef {Object} GrabberContext
 * @property {Boolean|Object} options Options object that specifies characteristics about the event listener. (@see addEventListener.options for detail)
 * @property {Array<GrabberListener>} listeners Registered listeners
 */

/**
 * Listen for the desired events
 * @param {Element} element DOM element to attach events listeners
 * @param {Editor} editor Editor to received down/move/up events
 * @param {Number} [offsetTop=0]
 * @param {Number} [offsetLeft=0]
 * @return {GrabberContext} Grabber context
 * @listens {Event} pointermove: a pointer moves, similar to touchmove or mousemove.
 * @listens {Event} pointerdown: a pointer is activated, or a device button held.
 * @listens {Event} pointerup: a pointer is deactivated, or a device button released.
 * @listens {Event} pointerover: a pointer has moved onto an element.
 * @listens {Event} pointerout: a pointer is no longer on an element it once was.
 * @listens {Event} pointerenter: a pointer enters the bounding box of an element.
 * @listens {Event} pointerleave: a pointer leaves the bounding box of an element.
 * @listens {Event} pointercancel: a pointer will no longer generate events.
 */
function attach(element, editor, offsetTop = 0, offsetLeft = 0) {

  function unfocus() {
    if (window.getSelection().type !== 'None') {
      window.getSelection().removeAllRanges();
    }
  }

  function pointerDownHandler(evt) {
    // Trigger a pointerDown
    // evt.target.id === editor.domElement.id ||
    const pointerDownOnEditor = evt.target.classList.contains(
      'mw-capturing-canvas'
    );
    if (
      !this.activePointerId &&
      evt.button !== 2 &&
      evt.buttons !== 2 &&
      pointerDownOnEditor
    ) {
      // Ignore right click
      this.activePointerId = evt.pointerId;
      this.startPoint = extractPoint(
        evt,
        element,
        editor.configuration,
        offsetTop,
        offsetLeft
      );
      // Hack for iOS 9 Safari : pointerId has to be int so -1 if > max value
      const pointerId = evt.pointerId > 2147483647 ? -1 : evt.pointerId;
      unfocus();
      evt.stopPropagation();
      editor.pointerDown(this.startPoint, evt.pointerType, pointerId);
    }
  }

  function pointerMoveHandler(evt) {
    // Trigger a pointerMove
    // Only considering the active pointer
    if (
      this.activePointerId !== undefined &&
      this.activePointerId === evt.pointerId
    ) {
      // const t = t1 - t0
      // console.log(t)
      // console.log(count++)
      // t0 = t1
      unfocus();
      const point = extractPoint(
        evt,
        element,
        editor.configuration,
        offsetTop,
        offsetLeft
      );
      editor.pointerMove(point);
    }
  }

  function pointerUpHandler(evt) {
    // Trigger a pointerUp
    if (
      this.activePointerId !== undefined &&
      this.activePointerId === evt.pointerId
    ) {
      // evt.stopPropagation()
      // Only considering the active pointer
      this.activePointerId = undefined; // Managing the active pointer
      const point = extractPoint(
        evt,
        element,
        editor.configuration,
        offsetTop,
        offsetLeft
      );
      editor.pointerUp(point);
    }
  }

  const context = {
    options: editor.configuration.listenerOptions,
    listeners: [
      {
        types: ['pointerdown'],
        listener: pointerDownHandler
      },
      {
        types: ['pointermove'],
        listener: pointerMoveHandler
      },
      {
        types: ['pointerup', 'pointerleave', 'pointercancel'],
        listener: pointerUpHandler
      }
    ]
  };

  // 'pointerout',
  context.listeners.forEach(item => {
    item.types.forEach(type =>
      element.addEventListener(type, item.listener, context.options)
    );
  });
  return context
}

function detach(element, context) {
  context.listeners.forEach(item => {
    item.types.forEach(type =>
      element.removeEventListener(type, item.listener, context.options)
    );
  });
}

var PointerEventGrabber = /*#__PURE__*/Object.freeze({
  attach: attach,
  detach: detach
});

/**
 * Calculates an array containing points representing a cardinal spline through given point array.
 * Points must be arranged as: [x1, y1, x2, y2, ..., xn, yn].
 *
 * The points for the cardinal spline are returned as a new array.
 *
 * @param {Array} points - point array
 * @param {Number} [tension=0.5] - tension. Typically between [0.0, 1.0] but can be exceeded
 * @param {Number} [numOfSeg=20] - number of segments between two points (line resolution)
 * @param {Boolean} [close=false] - Close the ends making the line continuous
 * @returns {Float32Array} New array with the calculated points that was added to the path
 */
function getCurvePoints(_points, tension = 0.5, numOfSeg = 20, close) {
  const points = [];

  _points.x.forEach((xx, i) => {
    points.push(xx, _points.y[i]);
  });

  let i = 1;
  let l = points.length;
  let rPos = 0;
  let rLen = (l - 2) * numOfSeg + 2 + (close ? 2 * numOfSeg : 0);
  let res = new Float32Array(rLen);
  let cache = new Float32Array((numOfSeg + 2) * 4);
  let cachePtr = 4;

  let pts = points.slice(0);

  if (close) {
    pts.unshift(points[l - 1]); // insert end point as first point
    pts.unshift(points[l - 2]);
    pts.push(points[0], points[1]); // first point as last point
  } else {
    pts.unshift(points[1]); // copy 1. point and insert at beginning
    pts.unshift(points[0]);
    pts.push(points[l - 2], points[l - 1]); // duplicate end-points
  }

  // cache inner-loop calculations as they are based on t alone
  cache[0] = 1; // 1,0,0,0

  for (; i < numOfSeg; i++) {
    var st = i / numOfSeg,
      st2 = st * st,
      st3 = st2 * st,
      st23 = st3 * 2,
      st32 = st2 * 3;

    cache[cachePtr++] = st23 - st32 + 1; // c1
    cache[cachePtr++] = st32 - st23; // c2
    cache[cachePtr++] = st3 - 2 * st2 + st; // c3
    cache[cachePtr++] = st3 - st2; // c4
  }

  cache[++cachePtr] = 1; // 0,1,0,0

  // calc. points
  parse(pts, cache, l);

  if (close) {
    //l = points.length;
    pts = [];
    pts.push(points[l - 4], points[l - 3], points[l - 2], points[l - 1]); // second last and last
    pts.push(points[0], points[1], points[2], points[3]); // first and second
    parse(pts, cache, 4);
  }

  function parse(pts, cache, l) {
    for (var i = 2, t; i < l; i += 2) {
      var pt1 = pts[i],
        pt2 = pts[i + 1],
        pt3 = pts[i + 2],
        pt4 = pts[i + 3],
        t1x = (pt3 - pts[i - 2]) * tension,
        t1y = (pt4 - pts[i - 1]) * tension,
        t2x = (pts[i + 4] - pt1) * tension,
        t2y = (pts[i + 5] - pt2) * tension;

      for (t = 0; t < numOfSeg; t++) {
        var c = t << 2, //t * 4;
          c1 = cache[c],
          c2 = cache[c + 1],
          c3 = cache[c + 2],
          c4 = cache[c + 3];

        res[rPos++] = c1 * pt1 + c2 * pt3 + c3 * t1x + c4 * t2x;
        res[rPos++] = c1 * pt2 + c2 * pt4 + c3 * t1y + c4 * t2y;
      }
    }
  }

  // add last point
  l = close ? 0 : points.length - 2;
  res[rPos++] = points[l];
  res[rPos] = points[l + 1];

  const resPoints = [];
  res.slice(-numOfSeg * 2).forEach((p, i) => {
    if (i % 2 === 0) {
      resPoints.push({
        x: p
      });
    } else {
      resPoints[resPoints.length - 1].y = p;
    }
  });

  return resPoints
}

// import { smooth } from '../util/PathCalculation'

/**
 * Stroke symbol
 * @typedef {Object} Stroke
 * @property {String} type=stroke Symbol type, 'stroke' for stroke
 * @property {String} pointerType=undefined Pointer type
 * @property {Number} pointerId=undefined Pointer id
 * @property {Array<Number>} x=[] X coordinates
 * @property {Array<Number>} y=[] Y coordinates
 * @property {Array<Number>} t=[] Timestamps matching x,y coordinates
 * @property {Array<Number>} p=[] Pressure
 * @property {Array<Number>} l=[] Length from origin
 * @property {Number} width=0 (for rendering) Pen/brush width
 * @property {String} color=undefined (for rendering) Pen/brush color
 */

/**
 * pointerEvents symbol
 * @typedef {Object} pointerEvents
 * @property {String} type=pointerEvents Symbol type, 'pointerEvents' for pointerEvents
 * @property {Boolean} processGestures=False indicates if the gestures have to be processed
 * @property {Array<Stroke>} events=[] the events to process
 */

function computeDistance(x, y, xArray, yArray, lastIndexPoint) {
  const distance = Math.sqrt(
    Math.pow(y - yArray[lastIndexPoint - 1], 2) +
      Math.pow(x - xArray[lastIndexPoint - 1], 2)
  );
  return isNaN(distance) ? 0 : distance
}

function computeLength(x, y, xArray, yArray, lArray, lastIndexPoint) {
  const length =
    lArray[lastIndexPoint - 1] +
    computeDistance(x, y, xArray, yArray, lastIndexPoint);
  return isNaN(length) ? 0 : length
}
/**
 * 计算模拟压力
 */
function computePressure(x, y, xArray, yArray, lArray, lastIndexPoint) {
  let ratio = 0.75;
  const distance = computeDistance(x, y, xArray, yArray, lastIndexPoint);
  const length = computeLength(x, y, xArray, yArray, lArray, lastIndexPoint);

  if (length === 0) {
    ratio = 0.75;
  } else if (distance === length) {
    ratio = 1;
  } else if (distance < 3) {
    ratio = 0.85;
  } else if (distance < 10) {
    ratio = 0.15 + Math.pow(0.1 * distance, 0.4); // 0.4
  } else if (distance > length - 10) {
    ratio = 0.2 + Math.pow(0.1 * (length - distance), 0.4);
  } else if (distance > 50) {
    ratio = 1.125;
  }
  const f = ratio * Math.max(0.25, 1.0 - 0.1 * Math.pow(distance, 0.4));
  const pressure = isNaN(parseFloat(f)) ? 0.5 : f;
  return pressure
}

function filterPointByAcquisitionDelta(x, y, xArray, yArray, width) {
  let delta = 2 + Math.min(width, 15);
  let ret = false;
  if (
    xArray.length === 0 ||
    yArray.length === 0 ||
    Math.abs(xArray[xArray.length - 1] - x) >= delta ||
    Math.abs(yArray[yArray.length - 1] - y) >= delta
  ) {
    ret = true;
  }
  return ret
}

/**
 * Create a new stroke
 * @param {Object} properties Properties to be applied to the stroke.
 * @return {Stroke} New stroke with properties for quadratics draw
 */
function createStrokeComponent(properties) {
  const defaultStroke = {
    type: 'stroke',
    x: [],
    y: [],
    t: [],
    p: [],
    l: [],
    b: [],
    r: [],
    // s: 1,
    _x: [],
    _y: [],
    // center: { x: 0, y: 0 },
    // delta: { x: 0, y: 0 },
    width: 0
  };
  // 新的数据格式
  // const defaultStroke = {
  //   type: 'stroke',
  //   subType: '', // pen, pencil, brush
  //   startTime: 0,
  //   x: [],
  //   y: [],
  //   t: [],
  //   p: [],
  //   b: [],
  //   s: 1,
  //   _l: [],
  //   _r: [],
  //   _x: [],
  //   _y: [],
  //   center: { x: 0, y: 0 },
  //   delta: { x: 0, y: 0 },
  //   size: 0,
  //   opacity: 1
  // }
  return Object.assign({}, defaultStroke, properties)
}

function addPointToStroke(stroke, point, origin) {
  if (origin) {
    stroke._x.push(point.x);
    stroke._y.push(point.y);
  }
  stroke.x.push(point.x);
  stroke.y.push(point.y);
  stroke.t.push(point.t);
  stroke.p.push(
    point.p ||
      computePressure(
        point.x,
        point.y,
        stroke.x,
        stroke.y,
        stroke.l,
        stroke.x.length - 1
      )
  );
  // stroke.p.push(strokeType ? point.p || 0.35 : 1)
  stroke.b.push(0);
  stroke.l.push(
    computeLength(
      point.x,
      point.y,
      stroke.x,
      stroke.y,
      stroke.l,
      stroke.x.length - 1
    )
  );
  return stroke
}

/**
 * Mutate a stroke by adding a point to it.
 * @param {Stroke} stroke Current stroke
 * @param {{x: Number, y: Number, t: Number}} point Point to add
 * @return {Stroke} Updated stroke
 */
function addPoint(stroke, point) {
  const strokeReference = stroke;
  if (
    filterPointByAcquisitionDelta(
      point.x,
      point.y,
      strokeReference.x,
      strokeReference.y,
      strokeReference.width
    )
  ) {
    let points = [point];
    const l = computeDistance(
      point.x,
      point.y,
      strokeReference._x,
      strokeReference._y,
      strokeReference._x.length - 1
    );

    // 笔迹进行样条拟合
    if (
      (stroke.type === 'brush' || stroke.type === 'pencil') &&
      strokeReference.x.length > 1 &&
      l > stroke.width
    ) {
      console.log('样条插值');
      const p =
        point.p ||
        computePressure(
          point.x,
          point.y,
          strokeReference._x,
          strokeReference._y,
          strokeReference.l,
          strokeReference._x.length - 1
        );
      const t = point.t;
      const lastP = strokeReference.p[strokeReference.p.length - 1];
      const lastT = strokeReference.t[strokeReference.t.length - 1];
      const numOfSeg = 2;
      const curvePoints = getCurvePoints(
        {
          x: strokeReference._x.slice(-3).concat(point.x),
          y: strokeReference._y.slice(-3).concat(point.y)
        },
        0.6,
        numOfSeg
      );
      const stepP = (p - lastP) / (curvePoints.length || 1);
      const stepT = (t - lastT) / (curvePoints.length || 1);
      curvePoints.forEach((cp, i) => {
        cp.p = lastP + stepP * (i + 1);
        cp.t = lastT + Math.floor(stepT * (i + 1));
      });
      points = curvePoints;
    }

    // console.log(point)
    // console.log('============')

    points.forEach((p, i) => {
      addPointToStroke(strokeReference, p, i === points.length - 1);
    });

    // addPointToStroke(strokeReference, point, true)

    // repair stroke
    if (stroke.type === 'pen' && !point.p) {
      const length = strokeReference.p.length;
      let startP = 0.75;
      let endP = 0.35;
      let step = 6;
      let stepP = 0.1;
      if (length > step) {
        const index = length - step - 1;
        startP = computePressure(
          strokeReference.x[index],
          strokeReference.y[index],
          strokeReference.x.slice(0, index + 1),
          strokeReference.y.slice(0, index + 1),
          strokeReference.l.slice(0, index + 1),
          index
        );
        const lastP = strokeReference.p[index - 1];
        const diffP = startP - lastP;
        if (Math.abs(diffP) > 0.125) {
          startP = lastP + (0.125 * diffP) / Math.abs(diffP);
        }
        // startP = 1
        endP = Math.max(endP, startP - 0.05 * step);
      } else {
        step = length - 1;
      }
      if (step) {
        stepP = (startP - endP) / step;
      }
      for (let i = 0; i <= step; i++) {
        strokeReference.p[length - i - 1] = endP + stepP * i;
      }
    }

    // if (
    //   (stroke.type === 'pencil' || stroke.type === 'brush') &&
    //   strokeReference.x.length > 2
    // ) {
    //   const path = smooth({
    //     x: strokeReference._x,
    //     y: strokeReference._y,
    //     p: strokeReference.p
    //   })
    //   strokeReference.x = path.x
    //   strokeReference.y = path.y
    // }
  }
  return strokeReference
}

/**
 * Extract point by index
 * @param {Stroke} stroke Current stroke
 * @param {Number} index Zero-based index
 * @return {{x: Number, y: Number, t: Number, p: Number, l: Number}} Point with properties for quadratics draw
 */
function getPointByIndex(context, stroke, index) {
  let point;
  if (index !== undefined && index >= 0 && index < stroke.x.length) {
    let x = stroke.x[index];
    let y = stroke.y[index];
    if (x < 2 && y < 2) {
      x = x * context.canvas.offsetWidth;
      y = y * context.canvas.offsetHeight;
    }
    point = {
      x,
      y,
      t: stroke.t[index],
      p: stroke.p[index],
      b: stroke.b ? stroke.b[index] : undefined
    };
    if (stroke.r) {
      point.r = stroke.r[index];
    }
  }
  return point
}

/** ===============================================================================================
 * Compute quadratics control points
 * ============================================================================================= */

/**
 *
 * @param {{x: Number, y: Number, p: Number}} point
 * @param angle
 * @param width
 * @return {[{x: Number, y: Number},{x: Number, y: Number}]}
 */
function computeLinksPoints(point, angle, width) {
  const radius = point.p * width;
  return [
    {
      x: point.x - Math.sin(angle) * radius,
      y: point.y + Math.cos(angle) * radius
    },
    {
      x: point.x + Math.sin(angle) * radius,
      y: point.y - Math.cos(angle) * radius
    }
  ]
}

/**
 *
 * @param {{x: Number, y: Number, p: Number}} point1
 * @param {{x: Number, y: Number, p: Number}} point2
 * @return {{x: Number, y: Number, p: Number}}
 */
function computeMiddlePoint(point1, point2) {
  return {
    x: (point2.x + point1.x) / 2,
    y: (point2.y + point1.y) / 2,
    p: (point2.p + point1.p) / 2
  }
}

function computePoint(point1, point2, k = 0.5) {
  return {
    x: point1.x - (point1.x - point2.x) * k,
    y: point1.y - (point1.y - point2.y) * k,
    p: point1.p - (point1.p - point2.p) * k
  }
}

/**
 *
 * @param {{x: Number, y: Number}} begin
 * @param {{x: Number, y: Number}} end
 * @return {Number}
 */
function computeAxeAngle(begin, end) {
  return Math.atan2(end.y - begin.y, end.x - begin.x)
}

/**
 * Define how a stroke should be drawn
 * @typedef {Object} Stroker
 * @property {function(): StrokerInfo} getInfo Get some information about this stroker
 * @property {function(context: Object, stroke: Stroke)} drawStroke Render a stroke on the current context.
 */

function computeDistance$1(point1, point2) {
  const distance = Math.sqrt(
    Math.pow(point2.y - point1.y, 2) + Math.pow(point2.x - point1.x, 2)
  );
  return isNaN(distance) ? 0 : distance
}

// function computeQuadrant(begin, end) {
//   const beginY = -begin.y
//   const endY = -end.y
//   let xx = 0
//   if (end.x > begin.x && endY === beginY) {
//     xx = 5
//   } else if (end.x === begin.x && endY < beginY) {
//     xx = 6
//   } else if (end.x < begin.x && endY === beginY) {
//     xx = 7
//   } else if (end.x === begin.x && endY > beginY) {
//     xx = 8
//   }
//   return xx
// }

function renderArc(context, center, radius) {
  context.arc(center.x, center.y, radius, 0, Math.PI * 2, true);
}

function renderLine(context, begin, end, width) {
  const linkPoints1 = computeLinksPoints(
    begin,
    computeAxeAngle(begin, end),
    width
  );
  const linkPoints2 = computeLinksPoints(
    end,
    computeAxeAngle(begin, end),
    width
  );

  context.moveTo(linkPoints1[0].x, linkPoints1[0].y);
  context.lineTo(linkPoints2[0].x, linkPoints2[0].y);
  context.lineTo(linkPoints2[1].x, linkPoints2[1].y);
  context.lineTo(linkPoints1[1].x, linkPoints1[1].y);
}

function renderFinal(context, begin, end, width) {
  const ARCSPLIT = 6;
  const angle = computeAxeAngle(begin, end);
  const linkPoints = computeLinksPoints(end, angle, width);
  context.moveTo(linkPoints[0].x, linkPoints[0].y);
  for (let i = 1; i <= ARCSPLIT; i++) {
    const newAngle = angle - (i * Math.PI) / ARCSPLIT;
    context.lineTo(
      end.x - end.p * width * Math.sin(newAngle),
      end.y + end.p * width * Math.cos(newAngle)
    );
  }
}

/**
 * 绘制二次贝塞尔曲线
 * @param {*} context
 * @param {*} begin
 * @param {*} end
 * @param {*} ctrl
 * @param {*} width
 */
function renderQuadratic(context, begin, end, ctrl, width, drawCurrent) {
  const linkPoints1 = computeLinksPoints(
    begin,
    computeAxeAngle(begin, ctrl),
    width
  );
  const linkPoints2 = computeLinksPoints(end, computeAxeAngle(ctrl, end), width);
  const linkPoints3 = computeLinksPoints(
    ctrl,
    computeAxeAngle(begin, end),
    width
  );

  context.moveTo(linkPoints1[0].x, linkPoints1[0].y);
  context.quadraticCurveTo(
    linkPoints3[0].x,
    linkPoints3[0].y,
    linkPoints2[0].x,
    linkPoints2[0].y
  );
  context.lineTo(linkPoints2[1].x, linkPoints2[1].y);
  context.quadraticCurveTo(
    linkPoints3[1].x,
    linkPoints3[1].y,
    linkPoints1[1].x,
    linkPoints1[1].y
  );
  if (drawCurrent) {
    // const q = computeQuadrant(linkPoints1[0], linkPoints1[1])
    // if (q === 5) {
    //   return
    // }
    const a = 0.6;
    const b = computeDistance$1(linkPoints1[0], linkPoints1[1]) / 2;
    const c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2), 2);
    const angle =
      computeAxeAngle(linkPoints1[0], linkPoints1[1]) - Math.atan(a / b);
    const x = linkPoints1[0].x + c * Math.cos(angle);
    const y = linkPoints1[0].y + c * Math.sin(angle);
    context.lineTo(x, y);
  }
}

/**
 * Draw a stroke on a canvas, using quadratics
 * @param {Object} context Current rendering context
 * @param {Stroke} stroke Current stroke to be drawn
 */
function drawStroke(context, stroke) {
  const contextReference = context;
  const length = stroke.x.length;
  const width = stroke.width;
  const color = stroke.color ? stroke.color : contextReference.strokeStyle;
  const firstPoint = getPointByIndex(context, stroke, 0);
  const nbquadratics = length - 2;

  contextReference.save();
  try {
    contextReference.beginPath();
    if (length < 2) {
      renderArc(contextReference, firstPoint, width * (firstPoint.p || 0.75));
    } else {
      renderArc(contextReference, firstPoint, width * firstPoint.p);
      renderLine(
        contextReference,
        firstPoint,
        computePoint(firstPoint, getPointByIndex(context, stroke, 1)),
        width
      );
      // renderLine(
      //   contextReference,
      //   firstPoint,
      //   computePoint(firstPoint, getPointByIndex(context, stroke, 1), 0.8),
      //   width
      // )

      for (let i = 0; i < nbquadratics; i++) {
        // renderLine(
        //   contextReference,
        //   getPointByIndex(context, stroke, i),
        //   getPointByIndex(context, stroke, i + 1),
        //   width
        // )
        const prev = getPointByIndex(context, stroke, i);
        const curr = getPointByIndex(context, stroke, i + 1);
        const next = getPointByIndex(context, stroke, i + 2);
        if (curr.b) {
          continue
        }
        renderQuadratic(
          contextReference,
          computePoint(prev, curr),
          computePoint(curr, next),
          curr,
          width
        );
        // const prev = getPointByIndex(context, stroke, i)
        // const curr = getPointByIndex(context, stroke, i + 1)
        // const next = getPointByIndex(context, stroke, i + 2)
        // if (curr.b) {
        //   continue
        // }
        // renderQuadratic(
        //   contextReference,
        //   computePoint(prev, curr, 0.8),
        //   computePoint(curr, next, 0.2),
        //   curr,
        //   width
        // )
        // renderLine(
        //   contextReference,
        //   computePoint(curr, next, 0.2),
        //   computePoint(curr, next, 0.8),
        //   width
        // )
      }

      // renderLine(
      //   contextReference,
      //   getPointByIndex(context, stroke, length - 2),
      //   getPointByIndex(context, stroke, length - 1),
      //   width
      // )
      // renderLine(
      //   contextReference,
      //   computePoint(
      //     getPointByIndex(context, stroke, length - 2),
      //     getPointByIndex(context, stroke, length - 1),
      //     0.8
      //   ),
      //   getPointByIndex(context, stroke, length - 1),
      //   width
      // )
      renderLine(
        contextReference,
        computePoint(
          getPointByIndex(context, stroke, length - 2),
          getPointByIndex(context, stroke, length - 1)
        ),
        getPointByIndex(context, stroke, length - 1),
        width
      );
      renderFinal(
        contextReference,
        getPointByIndex(context, stroke, length - 2),
        getPointByIndex(context, stroke, length - 1),
        width
      );
    }
    contextReference.closePath();

    contextReference.fillStyle = color;
    // contextReference.stroke()
    contextReference.fill();
  } finally {
    contextReference.restore();
  }
}

function drawCurrentStroke(context, stroke, { drawFinal, drawByPen }) {
  const contextReference = context;
  const length = stroke.x.length;
  const renderedLength = stroke.r.length;
  const width = stroke.width;
  const color = stroke.color;
  const firstPoint = getPointByIndex(context, stroke, 0);

  contextReference.save();
  try {
    contextReference.beginPath();

    if (length > 0 && renderedLength === 0) {
      renderArc(contextReference, firstPoint, width * firstPoint.p);
      if (drawByPen || length > 6) stroke.r.push(1);
    }
    if (length > 1 && renderedLength <= 1) {
      renderLine(
        contextReference,
        firstPoint,
        computePoint(firstPoint, getPointByIndex(context, stroke, 1)),
        width
      );
      if (drawByPen || length > 7) stroke.r.push(1);
    }
    if (length > 2) {
      for (let i = Math.max(renderedLength, 2); i < length; i++) {
        const prev = getPointByIndex(context, stroke, i - 2);
        const curr = getPointByIndex(context, stroke, i - 1);
        const next = getPointByIndex(context, stroke, i);
        renderQuadratic(
          contextReference,
          computePoint(prev, curr),
          computePoint(curr, next),
          curr,
          width,
          true
        );
        if (drawByPen || i < length - 6) stroke.r.push(1);
      }
      if (drawFinal) {
        const second2lastPoint = getPointByIndex(context, stroke, length - 2);
        const lastPoint = getPointByIndex(context, stroke, length - 1);
        renderLine(
          contextReference,
          computeMiddlePoint(second2lastPoint, lastPoint),
          lastPoint,
          width,
          true
        );
        renderFinal(contextReference, second2lastPoint, lastPoint, width);
        while (stroke.r.length < stroke.x.length) {
          stroke.r.push(1);
        }
      }
    }

    // contextReference.closePath()

    contextReference.fillStyle = color;
    // contextReference.stroke()
    contextReference.fill();
  } finally {
    contextReference.restore();
  }
}

var QuadraticStroker = /*#__PURE__*/Object.freeze({
  drawStroke: drawStroke,
  drawCurrentStroke: drawCurrentStroke
});

const THETA = Math.PI / 80;

function computeAxeAngle$1(begin, end) {
  return Math.atan2(end.y - begin.y, end.x - begin.x)
}

function computeQuadrant(begin, end) {
  const beginY = -begin.y;
  const endY = -end.y;
  let xx = 0;
  if (end.x > begin.x && endY < beginY) {
    xx = 1;
  } else if (end.x > begin.x && endY > beginY) {
    xx = 4;
  } else if (end.x < begin.x && endY > beginY) {
    xx = 3;
  } else if (end.x < begin.x && endY < beginY) {
    xx = 2;
  } else if (end.x > begin.x && endY === beginY) {
    xx = 5;
  } else if (end.x === begin.x && endY < beginY) {
    xx = 6;
  } else if (end.x < begin.x && endY === beginY) {
    xx = 7;
  } else if (end.x === begin.x && endY > beginY) {
    xx = 8;
  }
  return xx
}

function computeEllipseParam(ellipse) {
  const sin = Math.sin(ellipse.theta);
  const cos = Math.cos(ellipse.theta);
  const sin_2 = Math.pow(sin, 2);
  const cos_2 = Math.pow(cos, 2);
  const a1_2 = Math.pow(ellipse.a, 2);
  const b1_2 = Math.pow(ellipse.b, 2);
  const A = a1_2 * sin_2 + b1_2 * cos_2;
  const B = 2 * (b1_2 - a1_2) * sin * cos;
  const C = a1_2 * cos_2 + b1_2 * sin_2;
  const D = -2 * A * ellipse.x - B * ellipse.y;
  const E = -B * ellipse.x - 2 * C * ellipse.y;
  const F =
    A * Math.pow(ellipse.x, 2) +
    B * ellipse.x * ellipse.y +
    C * Math.pow(ellipse.y, 2) -
    a1_2 * b1_2;
  return { A, B, C, D, E, F }
}

function computeTangentPoint(A, B, C, D, E, F, k, b) {
  const aa = A + B * k + C * Math.pow(k, 2);
  const bb = B * b + 2 * C * k * b + D + E * k;
  const x = -bb / (2 * aa);
  const y = k * x + b;
  return { x, y }
}

function computeTangentPoint2(A, B, C, D, E, F, t) {
  const x = t;
  const y = -(B * t + E) / (2 * C);
  return { x, y }
}

function computeCommonTangent(ellipse1, ellipse2) {
  const { A, B, C, D, E, F } = computeEllipseParam(ellipse1);
  const param2 = computeEllipseParam(ellipse2);
  const k = (ellipse1.y - ellipse2.y) / (ellipse1.x - ellipse2.x);
  const param_a = Math.pow(B, 2) - 4 * A * C;
  const param_b = 2 * B * D + 4 * C * D * k - 4 * A * E - 2 * B * E * k;
  const param_c =
    Math.pow(D + E * k, 2) - 4 * (A + B * k + C * Math.pow(k, 2)) * F;
  const result_b1 =
    (-param_b + Math.sqrt(Math.pow(param_b, 2) - 4 * param_a * param_c, 2)) /
    (2 * param_a);
  const result_b2 =
    (-param_b - Math.sqrt(Math.pow(param_b, 2) - 4 * param_a * param_c, 2)) /
    (2 * param_a);
  const p1 = computeTangentPoint(A, B, C, D, E, F, k, result_b1);
  const q1 = computeTangentPoint(A, B, C, D, E, F, k, result_b2);
  const p2 = computeTangentPoint(
    param2.A,
    param2.B,
    param2.C,
    param2.D,
    param2.E,
    param2.F,
    k,
    result_b1
  );
  const q2 = computeTangentPoint(
    param2.A,
    param2.B,
    param2.C,
    param2.D,
    param2.E,
    param2.F,
    k,
    result_b2
  );
  return { p1, p2, q1, q2 }
}

function computeCommonTangent2(ellipse1, ellipse2) {
  const { A, B, C, D, E, F } = computeEllipseParam(ellipse1);
  const param2 = computeEllipseParam(ellipse2);
  const param_a = Math.pow(B, 2) - 4 * A * C;
  const param_b = 2 * B * E - 4 * C * D;
  const param_c = Math.pow(E, 2) - 4 * C * F;
  const t1 =
    (-param_b + Math.sqrt(Math.pow(param_b, 2) - 4 * param_a * param_c, 2)) /
    (2 * param_a);
  const t2 =
    (-param_b - Math.sqrt(Math.pow(param_b, 2) - 4 * param_a * param_c, 2)) /
    (2 * param_a);
  const p1 = computeTangentPoint2(A, B, C, D, E, F, t1);
  const q1 = computeTangentPoint2(A, B, C, D, E, F, t2);
  const p2 = computeTangentPoint2(
    param2.A,
    param2.B,
    param2.C,
    param2.D,
    param2.E,
    param2.F,
    t1
  );
  const q2 = computeTangentPoint2(
    param2.A,
    param2.B,
    param2.C,
    param2.D,
    param2.E,
    param2.F,
    t2
  );
  return { p1, p2, q1, q2 }
}

function createEllipse(point, a, b, theta) {
  return {
    x: point.x,
    y: -point.y,
    a: a,
    b: b,
    theta: -theta
  }
  // return {
  //   x: point.x,
  //   y: -point.y,
  //   a: a * point.p,
  //   b: b * point.p,
  //   theta: -theta
  // }
}

function renderEllipse(context, ellipse) {
  if (!context.ellipse) return
  const { x, y, a, b, theta } = ellipse;
  context.ellipse(x, -y, a, b, -theta, 0, Math.PI * 2);
}

function renderCurve(context, ellipse1, ellipse2) {
  const quadrant = computeQuadrant(ellipse1, ellipse2);
  const { p1, p2, q1, q2 } =
    quadrant === 6 || quadrant === 8
      ? computeCommonTangent2(ellipse1, ellipse2)
      : computeCommonTangent(ellipse1, ellipse2);

  if (quadrant === 1 || quadrant === 4 || quadrant === 5 || quadrant === 8) {
    context.moveTo(q1.x, -q1.y);
    context.lineTo(q2.x, -q2.y);
    context.ellipse(
      ellipse2.x,
      -ellipse2.y,
      ellipse2.a,
      ellipse2.b,
      -ellipse2.theta,
      ellipse2.theta - (Math.PI + computeAxeAngle$1(q2, ellipse2)),
      ellipse2.theta - (Math.PI + computeAxeAngle$1(p2, ellipse2))
    );
    context.lineTo(p1.x, -p1.y);
    context.lineTo(q1.x, -q1.y);
  } else if (
    quadrant === 2 ||
    quadrant === 3 ||
    quadrant === 7 ||
    quadrant === 6
  ) {
    context.moveTo(p1.x, -p1.y);
    context.lineTo(p2.x, -p2.y);
    context.ellipse(
      ellipse2.x,
      -ellipse2.y,
      ellipse2.a,
      ellipse2.b,
      -ellipse2.theta,
      ellipse2.theta - (Math.PI + computeAxeAngle$1(p2, ellipse2)),
      ellipse2.theta - (Math.PI + computeAxeAngle$1(q2, ellipse2))
    );
    context.lineTo(q1.x, -q1.y);
    context.lineTo(p1.x, -p1.y);
  }
}

function drawStroke$1(context, stroke) {
  const contextReference = context;
  const length = stroke.x.length;
  const width = stroke.width;
  const color = stroke.color ? stroke.color : contextReference.strokeStyle;
  const firstPoint = getPointByIndex(context, stroke, 0);
  const a = width * (stroke.longAxis || 0.75);
  const b = width * (stroke.shortAxis || 0.3);
  const theta = stroke.theta || THETA;

  contextReference.save();
  try {
    contextReference.beginPath();

    if (length < 2) ; else {
      renderEllipse(contextReference, createEllipse(firstPoint, a, b, theta));
      for (let i = 1; i < length; i++) {
        renderCurve(
          contextReference,
          createEllipse(getPointByIndex(context, stroke, i - 1), a, b, theta),
          createEllipse(getPointByIndex(context, stroke, i), a, b, theta)
        );
      }
    }

    contextReference.fillStyle = color;
    // contextReference.stroke()
    contextReference.fill();
  } finally {
    contextReference.restore();
  }
}

function drawCurrentStroke$1(context, stroke) {
  const contextReference = context;
  const length = stroke.x.length;
  const renderedLength = stroke.r.length;
  const width = stroke.width;
  const color = stroke.color ? stroke.color : contextReference.strokeStyle;
  const firstPoint = getPointByIndex(context, stroke, 0);
  const a = width * (stroke.longAxis || 0.75);
  const b = width * (stroke.shortAxis || 0.3);
  const theta = stroke.theta || THETA;

  contextReference.save();
  try {
    contextReference.beginPath();

    if (length && renderedLength === 0) {
      renderEllipse(contextReference, createEllipse(firstPoint, a, b, theta));
      stroke.r.push(1);
    } else {
      for (let i = renderedLength; i < length; i++) {
        renderCurve(
          contextReference,
          createEllipse(getPointByIndex(context, stroke, i - 1), a, b, theta),
          createEllipse(getPointByIndex(context, stroke, i), a, b, theta)
        );
        stroke.r.push(1);
      }
    }

    contextReference.fillStyle = color;
    // contextReference.stroke()
    contextReference.fill();
  } finally {
    contextReference.restore();
  }
}

var EllipseStroker = /*#__PURE__*/Object.freeze({
  drawStroke: drawStroke$1,
  drawCurrentStroke: drawCurrentStroke$1
});

function computeAxeAngle$2(begin, end) {
  return Math.atan2(end.y - begin.y, end.x - begin.x)
}

function renderArc$1(context, center, radius) {
  context.arc(center.x, center.y, radius, 0, Math.PI * 2);
}

function computeK(circle1, circle2) {
  const k = (circle1.y - circle2.y) / (circle1.x - circle2.x);
  if (k === Infinity) {
    return { k1: Infinity, k2: Infinity }
  }
  const l = Math.sqrt(
    Math.pow(circle1.y - circle2.y, 2) + Math.pow(circle1.x - circle2.x, 2)
  );
  const lA = Math.sqrt(Math.pow(l, 2) - Math.pow(circle1.r - circle2.r, 2));
  const lB = lA;
  const angle = Math.atan(-k);
  const angleDiff = Math.asin((circle1.r - circle2.r) / l);
  const angleA = angle + angleDiff;
  const angleB = angle - angleDiff;
  const xA = circle2.x - Math.cos(angleA) * lA;
  const yA = circle2.y + Math.sin(angleA) * lA;
  const xB = circle2.x - Math.cos(angleB) * lB;
  const yB = circle2.y + Math.sin(angleB) * lB;
  const k1 = (yA - circle2.y) / (xA - circle2.x);
  const k2 = (yB - circle2.y) / (xB - circle2.x);
  return { k1, k2 }
}

function computeB(param1, param2, k, s) {
  const a = -4;
  const b1 = 4 * k * param1.D - 4 * param1.E;
  const c1 =
    Math.pow(param1.D + param1.E * k, 2) - 4 * param1.F * (Math.pow(k, 2) + 1);
  const B1 = (-b1 + Math.sqrt(Math.pow(b1, 2) - 4 * a * c1, 2)) / (2 * a);
  const B2 = (-b1 - Math.sqrt(Math.pow(b1, 2) - 4 * a * c1, 2)) / (2 * a);
  if (s) {
    return { b1: B1, b2: B2 }
  }
  const b2 = 4 * k * param2.D - 4 * param2.E;
  const c2 =
    Math.pow(param2.D + param2.E * k, 2) - 4 * param2.F * (Math.pow(k, 2) + 1);
  const B3 = (-b2 + Math.sqrt(Math.pow(b2, 2) - 4 * a * c2, 2)) / (2 * a);
  const B4 = (-b2 - Math.sqrt(Math.pow(b2, 2) - 4 * a * c2, 2)) / (2 * a);
  const arr = [B1, B2, B3, B4];
  arr.sort();
  let temp = arr[0];
  for (let i = 1; i < 4; i++) {
    if (Math.abs(arr[i] - temp) < 0.00001) {
      return temp
    } else {
      temp = arr[i];
    }
  }
  console.log(B1, B2, B3, B4);
  return temp
}

function computeT(param1, param2, s) {
  const a1 = Math.sqrt(
    16 * Math.pow(param1.D, 2) + 16 * (Math.pow(param1.E, 2) - 4 * param1.F),
    2
  );
  const t1 = (4 * param1.D + a1) / -8;
  const t2 = (4 * param1.D - a1) / -8;
  if (s) {
    return { t1, t2 }
  }
  const a2 = Math.sqrt(
    16 * Math.pow(param2.D, 2) + 16 * (Math.pow(param2.E, 2) - 4 * param2.F),
    2
  );
  const t3 = (4 * param2.D + a2) / -8;
  const t4 = (4 * param2.D - a2) / -8;
  const arr = [t1, t2, t3, t4];
  arr.sort();
  let temp = arr[0];
  for (let i = 1; i < 4; i++) {
    if (Math.abs(arr[i] - temp) < 0.0000001) {
      return temp
    } else {
      temp = arr[i];
    }
  }
  return temp
}

function computeCommonTangent$1(circle1, circle2) {
  const quadrant = computeQuadrant$1(circle1, circle2);
  const param1 = computeParam(circle1);
  const param2 = computeParam(circle2);
  const { k1, k2 } = computeK(circle1, circle2);
  let b1, b2, q1x, q1y, q2x, q2y, p1x, p1y, p2x, p2y;
  if (
    k1 === Infinity ||
    k1 === -Infinity ||
    k2 === Infinity ||
    k2 === -Infinity
  ) {
    const { t1, t2 } = computeT(param1, param2, true);
    if (quadrant === 6) {
      return {
        q1: { x: t1, y: -param1.E / 2 },
        q2: { x: t1, y: -param2.E / 2 },
        p1: { x: t2, y: -param1.E / 2 },
        p2: { x: t2, y: -param2.E / 2 }
      }
    } else {
      return {
        q1: { x: t2, y: -param1.E / 2 },
        q2: { x: t2, y: -param2.E / 2 },
        p1: { x: t1, y: -param1.E / 2 },
        p2: { x: t1, y: -param2.E / 2 }
      }
    }
  }

  if (circle2.r === circle1.r) {
    const bb = computeB(param1, param2, k1, true);
    if (quadrant === 1 || quadrant === 4 || quadrant === 5 || quadrant === 8) {
      b1 = bb.b2;
      b2 = bb.b1;
    } else {
      b1 = bb.b1;
      b2 = bb.b2;
    }
  } else {
    b1 = computeB(param1, param2, k1);
    b2 = computeB(param1, param2, k2);
  }
  if (!b1) {
    const t = computeT(param1, param2);
    q1x = q2x = t;
    q1y = -param1.E / 2;
    q2y = -param2.E / 2;
  } else {
    q1x =
      -(2 * k1 * b1 + param1.D + param1.E * k1) / (2 * (Math.pow(k1, 2) + 1));
    q1y = k1 * q1x + b1;
    q2x =
      -(2 * k1 * b1 + param2.D + param2.E * k1) / (2 * (Math.pow(k1, 2) + 1));
    q2y = k1 * q2x + b1;
  }
  if (!b2) {
    const t = computeT(param1, param2);
    p1x = p2x = t;
    p1y = -param1.E / 2;
    p2y = -param2.E / 2;
  } else {
    p1x =
      -(2 * k2 * b2 + param1.D + param1.E * k2) / (2 * (Math.pow(k2, 2) + 1));
    p1y = k2 * p1x + b2;
    p2x =
      -(2 * k2 * b2 + param2.D + param2.E * k2) / (2 * (Math.pow(k2, 2) + 1));
    p2y = k2 * p2x + b2;
  }
  return {
    q1: { x: q1x, y: q1y },
    q2: { x: q2x, y: q2y },
    p1: { x: p1x, y: p1y },
    p2: { x: p2x, y: p2y }
  }
}

function computeParam(circle) {
  const D = -2 * circle.x;
  const E = -2 * circle.y;
  const F = (Math.pow(D, 2) + Math.pow(E, 2) - 4 * Math.pow(circle.r, 2)) / 4;
  return { D, E, F }
}

function createCircle(point, width) {
  return {
    x: point.x,
    y: -point.y,
    r: width
  }
}

function computeQuadrant$1(begin, end) {
  const beginY = -begin.y;
  const endY = -end.y;
  let xx = 0;
  if (end.x > begin.x && endY < beginY) {
    xx = 1;
  } else if (end.x > begin.x && endY > beginY) {
    xx = 4;
  } else if (end.x < begin.x && endY > beginY) {
    xx = 3;
  } else if (end.x < begin.x && endY < beginY) {
    xx = 2;
  } else if (end.x > begin.x && endY === beginY) {
    xx = 5;
  } else if (end.x === begin.x && endY < beginY) {
    xx = 6;
  } else if (end.x < begin.x && endY === beginY) {
    xx = 7;
  } else if (end.x === begin.x && endY > beginY) {
    xx = 8;
  }
  return xx
}

function renderCurve$1(context, circle1, circle2) {
  const quadrant = computeQuadrant$1(circle1, circle2);
  const { p1, p2, q1, q2 } =
    quadrant === 6 || quadrant === 8
      ? computeCommonTangent$1(circle1, circle2)
      : computeCommonTangent$1(circle1, circle2);

  context.moveTo(q1.x, -q1.y);
  context.lineTo(q2.x, -q2.y);
  context.arc(
    circle2.x,
    -circle2.y,
    circle2.r,
    2 * Math.PI - computeAxeAngle$2(circle2, q2),
    2 * Math.PI - computeAxeAngle$2(circle2, p2)
  );
  context.lineTo(p1.x, -p1.y);
  context.lineTo(q1.x, -q1.y);
}

function drawStroke$2(context, stroke) {
  const contextReference = context;
  const length = stroke.x.length;
  const width = stroke.width;
  const color = stroke.color ? stroke.color : contextReference.strokeStyle;
  const firstPoint = getPointByIndex(context, stroke, 0);

  contextReference.save();
  try {
    contextReference.beginPath();

    if (length < 2) {
      renderArc$1(contextReference, firstPoint, width);
    } else {
      renderArc$1(contextReference, firstPoint, width);
      for (let i = 1; i < length; i++) {
        renderCurve$1(
          contextReference,
          createCircle(getPointByIndex(context, stroke, i - 1), width),
          createCircle(getPointByIndex(context, stroke, i), width)
        );
      }
    }

    contextReference.fillStyle = color;
    // contextReference.stroke()
    contextReference.fill();
  } finally {
    contextReference.restore();
  }
}

const StrokeSymbols = {
  pen: 'pen',
  pencil: 'pencil',
  brush: 'brush'
};

/**
 * Draw a stroke symbol
 * @param {Object} context Current rendering context
 * @param {Stroke} stroke Stroke to be drawn
 */
function drawStroke$3(context, stroke) {
  if (stroke.type === 'pen') {
    drawStroke(context, stroke);
  } else if (stroke.type === 'pencil') {
    drawStroke$2(context, stroke);
  } else if (stroke.type === 'brush') {
    drawStroke$1(context, stroke);
  }
}

const pointer = new Image();

const EraserSymbols = {
  eraser: 'eraser',
  wiper: 'wiper'
};

function drawRoundedRect(ctx, _x, _y, width, height, r) {
  const x = _x - width / 2;
  const y = _y - height / 2;
  // ctx.beginPath()
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + width, y, x + width, y + r, r);
  ctx.arcTo(x + width, y + height, x + width - r, y + height, r);
  ctx.arcTo(x, y + height, x, y + height - r, r);
  ctx.arcTo(x, y, x + r, y, r);
  // ctx.closePath()
  // ctx.globalCompositeOperation = 'destination-out'
  // ctx.fill()
  // ctx.stroke()
}

function drawArcTo(ctx, x1, y1, x2, y2, radius) {
  ctx.arcTo(x1, y1, x2, y2, radius);
}

function drawRoundedStroke(ctx, p1, p2, width, height, radius, isCurrent) {
  let xx = 1,
    o1,
    o2,
    m1,
    n1,
    n2,
    q1,
    q2;
  const arc1 = Math.atan2(Math.abs(p1.y - p2.y), Math.abs(p2.x - p1.x));
  const arc2 = Math.atan2(Math.abs(p2.x - p1.x), Math.abs(p1.y - p2.y));
  const halfw = width / 2;
  const halfh = height / 2;
  if (p2.x > p1.x && p2.y < p1.y) {
    xx = 1; // 2点
  } else if (p2.x > p1.x && p2.y > p1.y) {
    xx = 4; // 4点
  } else if (p2.x < p1.x && p2.y > p1.y) {
    xx = 3; // 8点
  } else if (p2.x < p1.x && p2.y < p1.y) {
    xx = 2; // 10点
  } else if (p2.x > p1.x && p2.y === p1.y) {
    xx = 5; // 3点
  } else if (p2.x === p1.x && p2.y < p1.y) {
    xx = 6; // 12点
  } else if (p2.x < p1.x && p2.y === p1.y) {
    xx = 7; // 9点
  } else if (p2.x === p1.x && p2.y > p1.y) {
    xx = 8; // 6点
  }
  // ctx.beginPath()
  switch (xx) {
    case 1:
      o1 = {
        x: p1.x - (halfw - radius),
        y: p1.y - (halfh - radius)
      };
      o2 = {
        x: p2.x - (halfw - radius),
        y: p2.y - (halfh - radius)
      };
      q1 = {
        x: p1.x + (halfw - radius),
        y: p1.y + (halfh - radius)
      };
      q2 = {
        x: p2.x + (halfw - radius),
        y: p2.y + (halfh - radius)
      };
      m1 = {
        x: o1.x - Math.sin(arc1) * radius,
        y: o1.y - Math.cos(arc1) * radius
      };
      n1 = {
        x: q1.x + Math.sin(arc1) * radius,
        y: q1.y + Math.cos(arc1) * radius
      };
      n2 = {
        x: q2.x + Math.sin(arc1) * radius,
        y: q2.y + Math.cos(arc1) * radius
      };
      ctx.moveTo(m1.x, m1.y);
      drawArcTo(
        ctx,
        o2.x - Math.tan(arc1 / 2) * radius,
        o2.y - radius,
        o2.x,
        o2.y - radius,
        radius
      );
      drawArcTo(ctx, p2.x + halfw, p2.y - halfh, p2.x + halfw, o2.y, radius);
      drawArcTo(
        ctx,
        q2.x + radius,
        q2.y + Math.tanh(arc2 / 2) * radius,
        n2.x,
        n2.y,
        radius
      );
      ctx.lineTo(n1.x, n1.y);
      break
    case 2:
      o1 = {
        x: p1.x + (halfw - radius),
        y: p1.y - (halfh - radius)
      };
      o2 = {
        x: p2.x + (halfw - radius),
        y: p2.y - (halfh - radius)
      };
      q1 = {
        x: p1.x - (halfw - radius),
        y: p1.y + (halfh - radius)
      };
      q2 = {
        x: p2.x - (halfw - radius),
        y: p2.y + (halfh - radius)
      };
      m1 = {
        x: o1.x + Math.sin(arc1) * radius,
        y: o1.y - Math.cos(arc1) * radius
      };
      n1 = {
        x: q1.x - Math.sin(arc1) * radius,
        y: q1.y + Math.cos(arc1) * radius
      };
      n2 = {
        x: q2.x - Math.sin(arc1) * radius,
        y: q2.y + Math.cos(arc1) * radius
      };
      // ctx.moveTo(m1.x, m1.y)
      // drawArcTo(
      //   ctx,
      //   o2.x + Math.tan(arc1 / 2) * radius,
      //   o2.y - radius,
      //   o2.x,
      //   o2.y - radius,
      //   radius
      // )
      // drawArcTo(ctx, p2.x - halfw, p2.y - halfh, p2.x - halfw, o2.y, radius)
      // drawArcTo(
      //   ctx,
      //   p2.x - halfw,
      //   q2.y + Math.tanh(arc2 / 2) * radius,
      //   n2.x,
      //   n2.y,
      //   radius
      // )
      // ctx.lineTo(n1.x, n1.y)
      ctx.moveTo(n1.x, n1.y);
      drawArcTo(
        ctx,
        p2.x - halfw,
        q2.y + Math.tanh(arc2 / 2) * radius,
        p2.x - halfw,
        q2.y,
        radius
      );
      drawArcTo(ctx, p2.x - halfw, p2.y - halfh, o2.x, p2.y - halfh, radius);
      drawArcTo(
        ctx,
        o2.x + Math.tan(arc1 / 2) * radius,
        o2.y - radius,
        m1.x,
        m1.y,
        radius
      );
      ctx.lineTo(m1.x, m1.y);
      break
    case 3:
      o1 = {
        x: p1.x - (halfw - radius),
        y: p1.y - (halfh - radius)
      };
      o2 = {
        x: p2.x - (halfw - radius),
        y: p2.y - (halfh - radius)
      };
      q1 = {
        x: p1.x + (halfw - radius),
        y: p1.y + (halfh - radius)
      };
      q2 = {
        x: p2.x + (halfw - radius),
        y: p2.y + (halfh - radius)
      };
      m1 = {
        x: o1.x - Math.sin(arc1) * radius,
        y: o1.y - Math.cos(arc1) * radius
      };
      n1 = {
        x: q1.x + Math.sin(arc1) * radius,
        y: q1.y + Math.cos(arc1) * radius
      };
      n2 = {
        x: q2.x + Math.sin(arc1) * radius,
        y: q2.y + Math.cos(arc1) * radius
      };
      // ctx.moveTo(m1.x, m1.y)
      // drawArcTo(
      //   ctx,
      //   o2.x - radius,
      //   o2.y - Math.tan(arc2 / 2) * radius,
      //   o2.x - radius,
      //   o2.y,
      //   radius
      // )
      // drawArcTo(
      //   ctx,
      //   p2.x - halfw,
      //   p2.y + halfh,
      //   q2.x - radius,
      //   p2.y + halfh,
      //   radius
      // )
      // drawArcTo(
      //   ctx,
      //   q2.x + Math.tanh(arc1 / 2) * radius,
      //   p2.y + halfh,
      //   n2.x,
      //   n2.y,
      //   radius
      // )
      // ctx.lineTo(n1.x, n1.y)
      ctx.moveTo(n1.x, n1.y);
      drawArcTo(
        ctx,
        q2.x + Math.tanh(arc1 / 2) * radius,
        p2.y + halfh,
        q2.x,
        p2.y + halfh,
        radius
      );
      drawArcTo(ctx, p2.x - halfw, p2.y + halfh, p2.x - halfw, q2.y, radius);
      drawArcTo(
        ctx,
        o2.x - radius,
        o2.y - Math.tan(arc2 / 2) * radius,
        m1.x,
        m1.y,
        radius
      );
      ctx.lineTo(m1.x, m1.y);
      break
    case 4:
      o1 = {
        x: p1.x + (halfw - radius),
        y: p1.y - (halfh - radius)
      };
      o2 = {
        x: p2.x + (halfw - radius),
        y: p2.y - (halfh - radius)
      };
      q1 = {
        x: p1.x - (halfw - radius),
        y: p1.y + (halfh - radius)
      };
      q2 = {
        x: p2.x - (halfw - radius),
        y: p2.y + (halfh - radius)
      };
      m1 = {
        x: o1.x + Math.sin(arc1) * radius,
        y: o1.y - Math.cos(arc1) * radius
      };
      n1 = {
        x: q1.x - Math.sin(arc1) * radius,
        y: q1.y + Math.cos(arc1) * radius
      };
      n2 = {
        x: q2.x - Math.sin(arc1) * radius,
        y: q2.y + Math.cos(arc1) * radius
      };
      ctx.moveTo(m1.x, m1.y);
      drawArcTo(
        ctx,
        o2.x + radius,
        o2.y - Math.tan(arc2 / 2) * radius,
        o2.x + radius,
        o2.y,
        radius
      );
      drawArcTo(ctx, p2.x + halfw, p2.y + halfh, o2.x, p2.y + halfh, radius);
      drawArcTo(
        ctx,
        q2.x - Math.tanh(arc1 / 2) * radius,
        q2.y + radius,
        n2.x,
        n2.y,
        radius
      );
      ctx.lineTo(n1.x, n1.y);
      break
    case 5:
      ctx.moveTo(p1.x + halfw - radius, p1.y - halfh);
      ctx.arcTo(
        p2.x + halfw,
        p2.y - halfh,
        p2.x + halfw,
        p2.y - halfh + radius,
        radius
      );
      ctx.arcTo(
        p2.x + halfw,
        p2.y + halfh,
        p2.x + halfw - radius,
        p2.y + halfh,
        radius
      );
      ctx.lineTo(p1.x + halfw - radius, p1.y + halfh);
      break
    case 6:
      ctx.moveTo(p1.x - halfw, p1.y - halfh + radius);
      ctx.arcTo(
        p2.x - halfw,
        p2.y - halfh,
        p2.x - halfw + radius,
        p2.y - halfh,
        radius
      );
      ctx.arcTo(
        p2.x + halfw,
        p2.y - halfh,
        p2.x + halfw,
        p2.y - halfh + radius,
        radius
      );
      ctx.lineTo(p1.x + halfw, p1.y - halfh + radius);
      break
    case 7:
      // ctx.moveTo(p1.x - halfw + radius, p1.y - halfh)
      // ctx.arcTo(
      //   p2.x - halfw,
      //   p2.y - halfh,
      //   p2.x - halfw,
      //   p2.y - halfh + radius,
      //   radius
      // )
      // ctx.arcTo(
      //   p2.x - halfw,
      //   p2.y + halfh,
      //   p2.x - halfw + radius,
      //   p2.y + halfh,
      //   radius
      // )
      // ctx.lineTo(p1.x - halfw + radius, p1.y + halfh)
      ctx.moveTo(p1.x - halfw + radius, p1.y + halfh);
      ctx.arcTo(
        p2.x - halfw,
        p2.y + halfh,
        p2.x - halfw,
        p2.y + halfh - radius,
        radius
      );
      ctx.arcTo(
        p2.x - halfw,
        p2.y - halfh,
        p2.x - halfw + radius,
        p2.y - halfh,
        radius
      );
      ctx.lineTo(p1.x - halfw + radius, p1.y - halfh);
      break
    case 8:
      // ctx.moveTo(p1.x - halfw, p1.y + halfh - radius)
      // ctx.arcTo(
      //   p2.x - halfw,
      //   p2.y + halfh,
      //   p2.x - halfw + radius,
      //   p2.y + halfh,
      //   radius
      // )
      // ctx.arcTo(
      //   p2.x + halfw,
      //   p2.y + halfh,
      //   p2.x + halfw,
      //   p2.y + halfh - radius,
      //   radius
      // )
      // ctx.lineTo(p1.x + halfw, p1.y + halfh - radius)
      ctx.moveTo(p1.x + halfw, p1.y + halfh - radius);
      ctx.arcTo(
        p2.x + halfw,
        p2.y + halfh,
        p2.x + halfw - radius,
        p2.y + halfh,
        radius
      );
      ctx.arcTo(
        p2.x - halfw,
        p2.y + halfh,
        p2.x - halfw,
        p2.y + halfh - radius,
        radius
      );
      ctx.lineTo(p1.x - halfw, p1.y + halfh - radius);
      break
  }
  // ctx.closePath()
  // if (!isCurrent) {
  // ctx.globalCompositeOperation = 'destination-out'
  // ctx.fill()
  // }
}

function drawEraser(context, stroke, isCurrent) {
  const contextReference = context;
  const length = stroke.x.length;
  const color = 'black';
  const firstPoint = getPointByIndex(context, stroke, 0);
  contextReference.save();
  // debugger
  try {
    contextReference.beginPath();

    if (stroke.type === 'eraser') {
      const width = stroke.width;
      context.moveTo(firstPoint.x, firstPoint.y);
      for (let i = 0; i < length - 1; i++) {
        const nextPoint = getPointByIndex(
          context,
          stroke,
          i + 1
        );
        context.lineTo(nextPoint.x, nextPoint.y);
      }
      contextReference.lineCap = 'round';
      contextReference.lineJoin = 'round';
      contextReference.lineWidth = width;
      contextReference.strokeStyle = color;
      contextReference.globalCompositeOperation = 'destination-out';
      contextReference.stroke();
    } else {
      const pointerWidth =
        stroke.pointerWidth *
        (stroke.pageWidth ? context.canvas.offsetWidth / stroke.pageWidth : 1);
      const pointerHeight =
        stroke.pointerHeight *
        (stroke.pageHeight
          ? context.canvas.offsetHeight / stroke.pageHeight
          : 1);
      drawRoundedRect(
        contextReference,
        firstPoint.x,
        firstPoint.y,
        pointerWidth,
        pointerHeight,
        stroke.radius
      );

      for (let i = 0; i < length - 1; i++) {
        const currPoint = getPointByIndex(context, stroke, i);
        const nextPoint = getPointByIndex(
          context,
          stroke,
          i + 1
        );
        drawRoundedStroke(
          contextReference,
          currPoint,
          nextPoint,
          pointerWidth,
          pointerHeight,
          stroke.radius,
          isCurrent
        );
      }

      contextReference.closePath();
      contextReference.globalCompositeOperation = 'destination-out';
      contextReference.fillStyle = color;
      contextReference.fill();
      // contextReference.stroke()
    }
  } finally {
    contextReference.restore();
  }
}

function drawCurrentEraser(context, stroke, isCurrent) {
  const contextReference = context;
  const length = stroke.x.length;
  const renderedLength = stroke.r.length;
  const width = stroke.width;
  const color = 'black';
  const firstPoint = getPointByIndex(context, stroke, 0);
  contextReference.save();
  // debugger
  try {
    contextReference.beginPath();

    if (stroke.type === 'eraser') {
      context.moveTo(firstPoint.x, firstPoint.y);
      for (let i = 0; i < length - 1; i++) {
        const nextPoint = getPointByIndex(
          context,
          stroke,
          i + 1
        );
        context.lineTo(nextPoint.x, nextPoint.y);
      }
      contextReference.lineCap = 'round';
      contextReference.lineJoin = 'round';
      contextReference.lineWidth = width;
      contextReference.strokeStyle = color;
      contextReference.globalCompositeOperation = 'destination-out';
      contextReference.stroke();
    } else {
      if (length && renderedLength === 0) {
        drawRoundedRect(
          contextReference,
          firstPoint.x,
          firstPoint.y,
          stroke.pointerWidth,
          stroke.pointerHeight,
          stroke.radius
        );
        stroke.r.push(1);
      } else {
        for (let i = renderedLength - 1; i < length - 1; i++) {
          const currPoint = getPointByIndex(context, stroke, i);
          const nextPoint = getPointByIndex(
            context,
            stroke,
            i + 1
          );
          drawRoundedStroke(
            contextReference,
            currPoint,
            nextPoint,
            stroke.pointerWidth,
            stroke.pointerHeight,
            stroke.radius,
            isCurrent
          );
          stroke.r.push(1);
        }
      }
      // contextReference.closePath()
      // contextReference.beginPath()
      // for (let i = 0; i < length - 1; i++) {
      //   const currPoint = StrokeComponent.getPointByIndex(context, stroke, i)
      //   drawRoundedRect(
      //     contextReference,
      //     currPoint.x,
      //     currPoint.y,
      //     stroke.pointerWidth,
      //     stroke.pointerHeight,
      //     stroke.radius
      //   )
      // }
      contextReference.closePath();
      contextReference.fillStyle = color;
      contextReference.globalCompositeOperation = 'destination-out';
      contextReference.fill();
      // contextReference.stroke()
    }
  } finally {
    contextReference.restore();
  }
}

function mergeBounds(boundsA, boundsB) {
  return {
    minX: Math.min(boundsA.minX, boundsB.minX),
    maxX: Math.max(boundsA.maxX, boundsB.maxX),
    minY: Math.min(boundsA.minY, boundsB.minY),
    maxY: Math.max(boundsA.maxY, boundsB.maxY)
  }
}

function getLineBounds(line) {
  return {
    minX: Math.min(line.firstPoint.x, line.lastPoint.x),
    maxX: Math.max(line.firstPoint.x, line.lastPoint.x),
    minY: Math.min(line.firstPoint.y, line.lastPoint.y),
    maxY: Math.max(line.firstPoint.y, line.lastPoint.y)
  }
}

function getEllipseBounds(ellipse) {
  const angleStep = 0.02; // angle delta between interpolated points on the arc, in radian

  let z1 = Math.cos(ellipse.orientation);
  let z3 = Math.sin(ellipse.orientation);
  let z2 = z1;
  let z4 = z3;
  z1 *= ellipse.maxRadius;
  z2 *= ellipse.minRadius;
  z3 *= ellipse.maxRadius;
  z4 *= ellipse.minRadius;

  const n = Math.abs(ellipse.sweepAngle) / angleStep;

  const x = [];
  const y = [];

  for (let i = 0; i <= n; i++) {
    const angle = ellipse.startAngle + (i / n) * ellipse.sweepAngle;
    const alpha = Math.atan2(
      Math.sin(angle) / ellipse.minRadius,
      Math.cos(angle) / ellipse.maxRadius
    );

    const cosAlpha = Math.cos(alpha);
    const sinAlpha = Math.sin(alpha);

    x.push(ellipse.center.x + (z1 * cosAlpha - z4 * sinAlpha));
    y.push(ellipse.center.y + (z2 * sinAlpha + z3 * cosAlpha));
  }

  return {
    minX: Math.min(...x),
    maxX: Math.max(...x),
    minY: Math.min(...y),
    maxY: Math.max(...y)
  }
}

function getStrokeBounds(stroke) {
  const width = (stroke.width || 5) * 2;
  return {
    minX: Math.min(...stroke.x) - width,
    maxX: Math.max(...stroke.x) + width,
    minY: Math.min(...stroke.y) - width,
    maxY: Math.max(...stroke.y) + width
  }
}

/**
 * Get the box enclosing the given symbols
 * @param {Array} symbols Symbols to extract bounds from
 * @param {Bounds} [bounds] Starting bounds for recursion
 * @return {Bounds} Bounding box enclosing symbols
 */
function getSymbolsBounds(
  symbols,
  bounds = {
    minX: Number.MAX_VALUE,
    maxX: Number.MIN_VALUE,
    minY: Number.MAX_VALUE,
    maxY: Number.MIN_VALUE
  }
) {
  let boundsRef = bounds;
  boundsRef = symbols
    .filter(symbol => symbol.type === 'pen')
    .map(getStrokeBounds)
    .reduce(mergeBounds, boundsRef);
  boundsRef = symbols
    .filter(symbol => symbol.type === 'line')
    .map(getLineBounds)
    .reduce(mergeBounds, boundsRef);
  boundsRef = symbols
    .filter(symbol => symbol.type === 'ellipse')
    .map(getEllipseBounds)
    .reduce(mergeBounds, boundsRef);
  return boundsRef
}

/**
 * Renderer info
 * @typedef {Object} RendererInfo
 * @property {String} type Renderer type.
 */

/**
 * Default renderer
 * @typedef {Object} Renderer
 * @property {function(): RendererInfo} getInfo Get some information about this renderer
 * @property {function(element: Element, minHeight: Number, minWidth: Number): Object} attach Populate the DOM element to create rendering area.
 * @property {function(element: Element, context: Object)} detach Remove rendering area from the DOM element.
 * @property {function(context: Object, model: Model, stroker: Stroker)} resize Explicitly resize the rendering area.
 * @property {function(context: Object, model: Model, stroker: Stroker): Model} drawCurrentStroke Draw the model currentStroke.
 * @property {function(context: Object, model: Model, stroker: Stroker): Model} drawModel Draw the model defaultSymbols and recognizedSymbols.
 */

/**
 * Get info
 * @return {RendererInfo} Information about this renderer
 */
function getInfo() {
  return {
    type: 'canvas'
  }
}

function createCanvas(element, type) {
  // eslint-disable-next-line no-undef
  const canvas = element.ownerDocument.createElement('canvas');
  canvas.classList.add('mw-canvas', `mw-${type}-canvas`);
  canvas.style.position = 'absolute';
  canvas.style.top = '0px';
  canvas.style.left = '0px';
  if (type !== 'capturing') {
    canvas.style.pointerEvents = 'none';
  }
  element.appendChild(canvas);
  return canvas
}

// function createSVG(element, type) {
//   const container = element.ownerDocument.createElementNS(
//     'http://www.w3.org/2000/svg',
//     'svg'
//   )
//   container.classList.add(type)
//   element.appendChild(container)
//   return container
// }

function resizeContent(context) {
  const canvases = [
    context.capturingCanvas,
    context.temporaryCanvas,
    context.renderingCanvas
  ];
  const rendering = context.renderingCanvas;
  const domElement = rendering.parentNode;
  const width =
    domElement.clientWidth < context.minWidth
      ? context.minWidth
      : domElement.clientWidth;
  const height =
    domElement.clientHeight < context.minHeight
      ? context.minHeight
      : domElement.clientHeight;

  canvases.forEach(canvas => {
    /* eslint-disable no-param-reassign */
    canvas.width = width * context.pixelRatio;
    canvas.height = height * context.pixelRatio;
    canvas.style.width = `${width}px`;
    canvas.style.height = `${height}px`;
    /* eslint-enable no-param-reassign */
    canvas.getContext('2d').scale(context.pixelRatio, context.pixelRatio);
  });
  return context
}

function clearRect(canvas, bound) {
  if (bound) {
    canvas
      .getContext('2d')
      .clearRect(
        bound.minX - 40,
        bound.minY - 40,
        bound.maxX - bound.minX + 80,
        bound.maxY - bound.minY + 80
      );
  } else {
    canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
  }
}

/**
 * Attach the renderer to the DOM element
 * @param {Element} element DOM element to attach the rendering elements
 * @param {Number} [minHeight=0] Minimal height of the editor
 * @param {Number} [minWidth=0] Minimal width of the editor
 * @return {Object} The renderer context to give as parameter when a draw model will be call
 */
function attach$1(element, minHeight = 0, minWidth = 0) {
  // let pixelRatio = detectPixelRatio(element)
  let pixelRatio = 1;
  const renderingCanvas = createCanvas(element, 'rendering');
  const capturingCanvas = createCanvas(element, 'capturing');
  const temporaryCanvas = createCanvas(element, 'temporary');
  const renderingCanvasContext = renderingCanvas.getContext('2d');
  const capturingCanvasContext = capturingCanvas.getContext('2d');
  const temporaryCanvasContext = temporaryCanvas.getContext('2d');

  const context = {
    element,
    pixelRatio,
    minHeight,
    minWidth,
    renderingCanvas,
    renderingCanvasContext,
    capturingCanvas,
    capturingCanvasContext,
    temporaryCanvas,
    temporaryCanvasContext
  };

  resizeContent(context);
  return context
}

/**
 * Detach the renderer from the DOM element
 * @param {Element} element DOM element to attach the rendering elements
 * @param {Object} context Current rendering context
 */
function detach$1(element, context) {
  // context.resources.forEach(res => element.removeChild(res))
  element.removeChild(context.renderingCanvas);
  element.removeChild(context.capturingCanvas);
  element.removeChild(context.temporaryCanvas);
  // element.removeChild(context.animationCanvas)
}

/**
 * Update the rendering context size
 * @param {Object} context Current rendering context
 * @param {Model} model Current model
 * @return {Model}
 */
function resize(context, model) {
  return this.drawModel(resizeContent(context), model)
}

function drawSymbol(context, symbol) {
  const type = symbol.type;
  if (StrokeSymbols[type]) {
    drawStroke$3(context.renderingCanvasContext, symbol);
  } else if (EraserSymbols[type]) {
    drawEraser(context.renderingCanvasContext, symbol);
  }
}

/**
 * Draw the current stroke from the model
 * @param {Object} context Current rendering context
 * @param {Model} model Current model
 * @return {Model}
 */
function drawCurrentSymbol(context, model, opt = {}) {
  if (!model.currentStroke) return modelRef

  const modelRef = model;
  const currentStroke = modelRef.currentStroke;
  const pixelRatio = context.pixelRatio;
  const type = currentStroke && currentStroke.type;

  if (!currentStroke) return modelRef

  // modelRef.currentStroke.id = `stroke-${modelRef.rawStrokes.length}`
  if (EraserSymbols[type]) {
    const { minX, maxX, minY, maxY } = getStrokeBounds(currentStroke);
    clearRect(context.temporaryCanvas, { minX, maxX, minY, maxY });
    drawCurrentEraser(context.renderingCanvasContext, currentStroke, true);
    // drawEraserCursor(context.temporaryCanvasContext, currentStroke)
  } else if (StrokeSymbols[type]) {
    const { minX, maxX, minY, maxY } = getStrokeBounds(currentStroke);
    clearRect(context.temporaryCanvas, { minX, maxX, minY, maxY });
    drawStroke$3(context.temporaryCanvasContext, currentStroke, opt);

    if (opt.drawFinal) {
      context.capturingCanvasContext.drawImage(
        context.temporaryCanvas,
        minX * pixelRatio,
        minY * pixelRatio,
        (maxX - minX) * pixelRatio,
        (maxY - minY) * pixelRatio,
        minX,
        minY,
        maxX - minX,
        maxY - minY
      );
      clearRect(context.temporaryCanvas, { minX, maxX, minY, maxY });
    }
  }
  return modelRef
}

/**
 * Draw all symbols contained into the model
 * @param {Object} context Current rendering context
 * @param {Model} model Current model
 * @return {Model}
 */
function drawModel(context, model) {
  clearRect(context.renderingCanvas);
  model.rawStrokes.forEach(symbol => {
    drawSymbol(context, symbol);
  });
  clearRect(context.capturingCanvas);
  clearRect(context.temporaryCanvas);
  return model
}

var CanvasRenderer = /*#__PURE__*/Object.freeze({
  getInfo: getInfo,
  attach: attach$1,
  detach: detach$1,
  resize: resize,
  drawCurrentSymbol: drawCurrentSymbol,
  drawModel: drawModel
});

/**
 * Emits an event when the editor state change
 * @param {String} type
 * @param {Object} data
 * @emits {Event}
 */
function eventCallback(type, data) {
  // logger.info(`emitting ${type} event`, data);
  // We are making usage of a browser provided class
  // eslint-disable-next-line no-undef
  this.dispatchEvent(
    new CustomEvent(
      type,
      Object.assign(
        { bubbles: true, composed: true },
        data ? { detail: data } : undefined
      )
    )
  );
}

/**
 * Default behaviors
 * @type {Behaviors}
 */
const defaultBehaviors = {
  grabberList: [PointerEventGrabber],
  strokerList: [QuadraticStroker, EllipseStroker],
  rendererList: [CanvasRenderer],
  recognizerList: [],
  callbacks: [eventCallback],
  getBehaviorFromConfiguration: (behaviors, configuration) => {
    const behavior = {};
    behavior.grabber = PointerEventGrabber;
    behavior.renderer = CanvasRenderer;
    behavior.callbacks = behaviors.callbacks;
    return behavior
  }
};

/**
 * Generate behaviors
 * @param {Behaviors} behaviors Behaviors to be used
 * @return {Behaviors} Overridden behaviors
 */
function overrideDefaultBehaviors() {
  return defaultBehaviors
}

/**
 * Recognition positions
 * @typedef {Object} RecognitionPositions
 * @property {Number} [lastSentPosition=-1] Index of the last sent stroke.
 * @property {Number} [lastReceivedPosition=-1] Index of the last received stroke.
 * @property {Number} [lastRenderedPosition=-1] Last rendered recognized symbol position
 */

/**
 * Raw results
 * @typedef {Object} RawResults
 * @property {Object} convert=undefined The convert result
 * @property {Object} exports=undefined The exports output as return by the recognition service.
 */

/**
 * Editor model
 * @typedef {Object} Model
 * @property {Stroke} currentStroke=undefined Stroke in building process.
 * @property {Array<Stroke>} rawStrokes=[] List of captured strokes.
 * @property {Array} strokeGroups=[] Group of strokes with same pen style.
 * @property {Array<Object>} defaultSymbols=[] Default symbols, relative to the current recognition type.
 * @property {Array<Object>} recognizedSymbols=undefined Symbols to render (e.g. stroke, shape primitives, string, characters...).
 * @property {Object} exports=undefined Result of the export (e.g. mathml, latex, text...).
 * @property {RawResults} rawResults The recognition output as return by the recognition service.
 * @property {Number} creationTime Date of creation timestamp.
 * @property {Number} modificationTime=undefined Date of lastModification.
 */

/**
 * Bounding box
 * @typedef {Object} Bounds
 * @property {Number} minX Minimal x coordinate
 * @property {Number} maxX Maximal x coordinate
 * @property {Number} minY Minimal y coordinate
 * @property {Number} maxY Maximal y coordinate
 */

/**
 * Create a new model
 * @param {Configuration} [configuration] Parameters to use to populate default recognition symbols
 * @return {Model} New model
 */
function createModel(configuration) {
  // see @typedef documentation on top
  return {
    currentStroke: undefined,
    currentStrokes: [],
    rawStrokes: [],
    strokeGroups: [],
    defaultSymbols: [],
    recognizedSymbols: undefined,
    exports: undefined,
    rawResults: {
      convert: undefined,
      exports: undefined
    },
    creationTime: new Date().getTime(),
    modificationTime: undefined
  }
}

/**
 * Clear the model.
 * @param {Model} model Current model
 * @return {Model} Cleared model
 */
function clearModel(model) {
  const modelReference = model;
  modelReference.currentStroke = undefined;
  modelReference.rawStrokes = [];
  modelReference.strokeGroups = [];
  modelReference.recognizedSymbols = undefined;
  modelReference.exports = undefined;
  modelReference.rawResults.convert = undefined;
  modelReference.rawResults.exports = undefined;
  return modelReference
}

/**
 * Check if the model needs to be redrawn.
 * @param {Model} model Current model
 * @return {Boolean} True if the model needs to be redrawn, false otherwise
 */
function needRedraw(model) {
  const lastStroke = model.rawStrokes.slice(-1)[0];
  return !(lastStroke && lastStroke.pathChange)
}

/**
 * Mutate the model given in parameter by adding the new strokeToAdd.
 * @param {Model} model Current model
 * @param {Stroke} stroke Stroke to be added to pending ones
 * @return {Model} Updated model
 */
function addStroke(context, model, stroke) {
  // We use a reference to the model. The purpose here is to update the pending stroke only.
  const modelReference = model;
  if (!stroke.id) {
    stroke.id = stroke.type + '-' + Date.now();
  }
  if (stroke._x) delete stroke._x;
  if (stroke._y) delete stroke._y;
  if (stroke.l) delete stroke.l;
  if (stroke.r) delete stroke.r;
  if (stroke.pointer) delete stroke.pointer;

  if (!stroke.pageWidth || !stroke.pageHeight) {
    // 兼容旧数据
    const pageWidth = context.canvas.offsetWidth;
    const pageHeight = context.canvas.offsetHeight;
    if (stroke.x[0] > 2 && stroke.x[0] > 2) {
      stroke.x = stroke.x.map((x, i) => {
        if (x < 0) stroke.b[i] = 1;
        return x / pageWidth
      });
      stroke.y = stroke.y.map((y, i) => {
        if (y < 0) stroke.b[i] = 1;
        return y / pageHeight
      });
    }
    stroke.pageWidth = pageWidth;
    stroke.pageHeight = pageHeight;
  }

  modelReference.rawStrokes.push(stroke);
  return modelReference
}

/**
 * Mutate the model by adding a point and close the current stroke.
 * @param {Model} model Current model
 * @param {{x: Number, y: Number, t: Number}} point Captured point to create current stroke
 * @param {Object} properties Properties to be applied to the current stroke
 * @param {Number} [dpi=96] The screen dpi resolution
 * @return {Model} Updated model
 */
function initPendingStroke(model, point, properties, dpi = 96) {
  if (properties && properties['penWidth']) {
    Object.assign(properties, { width: properties['penWidth'] });
  }
  const modelReference = model;
  modelReference.currentStroke = createStrokeComponent(
    properties
  );
  modelReference.currentStroke = addPoint(
    modelReference.currentStroke,
    point
  );
  return modelReference
}

/**
 * Mutate the model by adding a point to the current pending stroke.
 * @param {Model} model Current model
 * @param {{x: Number, y: Number, t: Number}} point Captured point to be append to the current stroke
 * @return {Model} Updated model
 */
function appendToPendingStroke(model, point) {
  const modelReference = model;
  if (modelReference.currentStroke) {
    modelReference.currentStroke = addPoint(
      modelReference.currentStroke,
      point
    );
  }
  return modelReference
}

/**
 * Mutate the model by adding the new point on a initPendingStroke.
 * @param {Model} model Current model
 * @param {{x: Number, y: Number, t: Number}} point Captured point to be append to the current stroke
 * @param {PenStyle} penStyle
 * @return {Model} Updated model
 */
function endPendingStroke(model, context) {
  const modelReference = model;
  if (modelReference.currentStroke) {
    let currentStroke = modelReference.currentStroke;
    addStroke(context, modelReference, currentStroke);
    delete modelReference.currentStroke;
  }
  return modelReference
}

/**
 * Get the bounds of the current model.
 * @param {Model} model Current model
 * @return {Bounds} Bounding box enclosing the current drawn model
 */
function getBorderCoordinates(model) {
  let modelBounds = {
    minX: Number.MAX_VALUE,
    maxX: Number.MIN_VALUE,
    minY: Number.MAX_VALUE,
    maxY: Number.MIN_VALUE
  };
  modelBounds = getSymbolsBounds(model.rawStrokes, modelBounds);
  return modelBounds
}

/**
 * Clone model
 * @param {Model} model Current model
 * @return {Model} Clone of the current model
 */
function cloneModel(model) {
  const clonedModel = Object.assign({}, model);
  // We clone the properties that need to be. Take care of arrays.
  clonedModel.defaultSymbols = [...model.defaultSymbols];
  clonedModel.currentStroke = model.currentStroke
    ? Object.assign({}, model.currentStroke)
    : undefined;
  clonedModel.rawStrokes = [...model.rawStrokes];
  clonedModel.strokeGroups = JSON.parse(JSON.stringify(model.strokeGroups));
  clonedModel.exports = model.exports
    ? Object.assign({}, model.exports)
    : undefined;
  clonedModel.rawResults = Object.assign({}, model.rawResults);
  clonedModel.recognizedSymbols = model.recognizedSymbols
    ? [...model.recognizedSymbols]
    : undefined;
  return clonedModel
}

/**
 * Merge models
 * @param {...Model} models Models to merge (ordered)
 * @return {Model} Updated model
 */
function mergeModels(...models) {
  return models.reduce((a, b) => {
    const modelRef = a;
    modelRef.recognizedSymbols = b.recognizedSymbols;
    modelRef.rawResults = b.rawResults;
    modelRef.exports = b.exports;
    return modelRef
  })
}

var InkModel = /*#__PURE__*/Object.freeze({
  createModel: createModel,
  clearModel: clearModel,
  needRedraw: needRedraw,
  addStroke: addStroke,
  initPendingStroke: initPendingStroke,
  appendToPendingStroke: appendToPendingStroke,
  endPendingStroke: endPendingStroke,
  getBorderCoordinates: getBorderCoordinates,
  cloneModel: cloneModel,
  mergeModels: mergeModels
});

/**
 * Undo/redo context
 * @typedef {Object} UndoRedoContext
 * @property {Array<Model>} stack=[] List of processed models.
 * @property {Number} currentPosition=-1 Current model index into the stack.
 * @property {Number} maxSize Max size of the stack.
 * @property {Boolean} canUndo=false
 * @property {Boolean} canRedo=false
 */

/**
 * Create a new undo/redo context
 * @param {Configuration} configuration Current configuration
 * @return {UndoRedoContext} New undo/redo context
 */
function createUndoRedoContext(configuration) {
  return {
    stack: [],
    currentPosition: -1,
    maxSize: configuration.undoRedoMaxStackSize,
    canUndo: false,
    canRedo: false
  }
}

/**
 * Update the undo/redo state
 * @param {UndoRedoContext} undoRedoContext Current undo/redo context
 * @return {UndoRedoContext} Updated undo/redo context
 */
function updateUndoRedoState(undoRedoContext) {
  const undoRedoContextRef = undoRedoContext;
  undoRedoContextRef.canUndo = undoRedoContext.currentPosition > 0;
  undoRedoContextRef.canRedo =
    undoRedoContext.currentPosition < undoRedoContext.stack.length - 1;
  return undoRedoContextRef
}

function resetUndoRedoState(undoRedoContext) {
  const undoRedoContextRef = undoRedoContext;
  undoRedoContextRef.currentPosition = -1;
  undoRedoContextRef.canUndo = false;
  undoRedoContextRef.canRedo = false;
  undoRedoContextRef.stack = [];
  return undoRedoContextRef
}

/**
 * Undo/redo manager
 * @typedef {Object} UndoRedoManager
 * @property {function(undoRedoContext: UndoRedoContext, model: Model, callback: UpdateCallback)} updateModel Push the current model into the undo/redo context.
 * @property {function(undoRedoContext: UndoRedoContext, model: Model, callback: UpdateCallback)} undo Undo.
 * @property {function(undoRedoContext: UndoRedoContext, model: Model, callback: UpdateCallback)} redo Redo.
 * @property {function(undoRedoContext: UndoRedoContext, model: Model, callback: UpdateCallback)} clear Clear.
 */

/**
 * Get current model in stack
 * @param {UndoRedoContext} undoRedoContext Current undo/redo context
 * @param {function(err: Object, res: Model, types: ...String)} callback
 * @param {Boolean} [clone=true] Whether or not to clone the model
 * @param {...String} types
 */
function getModel(undoRedoContext, callback, clone = true, ...types) {
  const model = undoRedoContext.stack[undoRedoContext.currentPosition];
  callback(undefined, clone ? cloneModel(model) : model, ...types);
}

/**
 * Mutate the undoRedo stack by adding a new model to it.
 * @param {UndoRedoContext} undoRedoContext Current undo/redo context.
 * @param {Model} model Current model.
 * @param {function(err: Object, res: Model, types: ...String)} callback
 */
function updateModel(undoRedoContext, model, callback) {
  // Used to update the model with the recognition result if relevant
  const modelIndex = undoRedoContext.stack.findIndex(
    item =>
      item.modificationTime === model.modificationTime &&
      item.rawStrokes.length === model.rawStrokes.length
  );

  const modelReference = model;
  modelReference.modificationTime = new Date().getTime();

  const types = [];
  if (modelIndex > -1) {
    undoRedoContext.stack.splice(
      modelIndex,
      1,
      cloneModel(modelReference)
    );
    // logger.debug('model updated', modelReference)
  } else {
    const undoRedoContextReference = undoRedoContext;
    undoRedoContextReference.currentPosition += 1;
    undoRedoContextReference.stack = undoRedoContextReference.stack.slice(
      0,
      undoRedoContextReference.currentPosition
    );
    undoRedoContextReference.stack.push(cloneModel(modelReference));
    if (
      undoRedoContextReference.stack.length > undoRedoContextReference.maxSize
    ) {
      undoRedoContextReference.stack.shift();
      undoRedoContextReference.currentPosition--;
    }
    // logger.debug('model pushed', modelReference)
    types.push(Constants.EventType.CHANGED);
  }
  updateUndoRedoState(undoRedoContext);
  // logger.debug('undo/redo stack updated', undoRedoContext)
  getModel(undoRedoContext, callback, false, ...types);
}

/**
 * Undo
 * @param {UndoRedoContext} undoRedoContext Current undo/redo context.
 * @param {Model} model Current model.
 * @param {function(err: Object, res: Model, types: ...String)} callback
 */
function undo(undoRedoContext, model, callback) {
  const undoRedoContextReference = undoRedoContext;
  if (undoRedoContextReference.currentPosition > 0) {
    undoRedoContextReference.currentPosition -= 1;
    updateUndoRedoState(undoRedoContext);
    // logger.debug('undo index', undoRedoContextReference.currentPosition)
  }
  getModel(
    undoRedoContext,
    callback,
    true,
    Constants.EventType.CHANGED,
    Constants.EventType.EXPORTED
  );
}

/**
 * Redo
 * @param {UndoRedoContext} undoRedoContext Current undo/redo context.
 * @param {Model} model Current model.
 * @param {function(err: Object, res: Model, types: ...String)} callback
 */
function redo(undoRedoContext, model, callback) {
  const undoRedoContextReference = undoRedoContext;
  if (
    undoRedoContextReference.currentPosition <
    undoRedoContextReference.stack.length - 1
  ) {
    undoRedoContextReference.currentPosition += 1;
    updateUndoRedoState(undoRedoContext);
    // logger.debug('redo index', undoRedoContextReference.currentPosition)
  }
  getModel(
    undoRedoContext,
    callback,
    true,
    Constants.EventType.CHANGED,
    Constants.EventType.EXPORTED
  );
}

var UndoRedoManager = /*#__PURE__*/Object.freeze({
  getModel: getModel,
  updateModel: updateModel,
  undo: undo,
  redo: redo
});

function createCanvas$1(borderCoordinates, margin = 10) {
  // eslint-disable-next-line no-undef
  const browserDocument = document;
  const canvas = browserDocument.createElement('canvas');
  canvas.width =
    Math.abs(borderCoordinates.maxX - borderCoordinates.minX) + 2 * margin;
  canvas.style.width = `${canvas.width}px`;
  canvas.height =
    Math.abs(borderCoordinates.maxY - borderCoordinates.minY) + 2 * margin;
  canvas.style.height = `${canvas.height}px`;
  return canvas
}

/**
 * Generate a PNG image data url from the model
 * @param {Model} model Current model
 * @param {Number} [margin=10] Margins to apply around the image
 * @return {String} Image data string result
 */
function getImage(model, margin = 10) {
  if (model.rawStrokes.length > 0) {
    const borderCoordinates = getBorderCoordinates(model);
    const capturingCanvas = createCanvas$1(borderCoordinates, margin);
    const renderingCanvas = createCanvas$1(borderCoordinates, margin);
    const renderStructure = {
      renderingCanvas,
      renderingCanvasContext: renderingCanvas.getContext('2d'),
      capturingCanvas,
      capturingCanvasContext: capturingCanvas.getContext('2d')
    };
    // Change canvas origin
    renderStructure.renderingCanvasContext.translate(
      -borderCoordinates.minX + margin,
      -borderCoordinates.minY + margin
    );
    drawModel(renderStructure, model);
    return renderStructure.renderingCanvas.toDataURL('image/png')
  }
  return null
}

/* eslint-disable no-underscore-dangle */

const EventType = Constants.EventType;

const requestAnimFrame = (function() {
  return (
    window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function(callback) {
      window.setTimeout(callback, 1000 / 60);
    }
  )
})();

function triggerCallbacks(editor, data, ...types) {
  types.forEach(type => {
    switch (type) {
      case EventType.RENDERED:
        break // Internal use only
      case EventType.UNDO:
      case EventType.REDO:
      case EventType.CLEAR:
        editor.callbacks.forEach(callback =>
          callback.call(editor.domElement, type)
        );
        break
      case EventType.LOADED:
      case EventType.CHANGED:
        editor.callbacks.forEach(callback =>
          callback.call(editor.domElement, type, {
            initialized: editor.initialized,
            canUndo: editor.canUndo,
            canRedo: editor.canRedo,
            canClear: editor.canClear,
            isEmpty: editor.isEmpty
          })
        );
        break
      case EventType.ERROR:
        editor.callbacks.forEach(callback =>
          callback.call(editor.domElement, type, data)
        );
        break
      case 'pointer-down':
      case 'pointer-move':
      case 'pointer-up':
        editor.callbacks.forEach(callback =>
          callback.call(editor.domElement, type, data)
        );
        break
      default:
        break
    }
  });
}

/**
 * Manage recognized model
 * @param {Editor} editor
 * @param {Model} model
 * @param {...String} types
 */
function manageRecognizedModel(editor, model, ...types) {
  const editorRef = editor;
  const modelRef = model;
  if (modelRef.creationTime === editor.model.creationTime) {
    if (modelRef.rawStrokes.length === editor.model.rawStrokes.length) {
      editorRef.model = mergeModels(editorRef.model, modelRef);
      if (
        needRedraw(editorRef.model) ||
        types.includes(EventType.RENDERED)
      ) {
        editor.renderer.drawModel(editor.rendererContext, editorRef.model);
      }
    } else {
      editorRef.model = modelRef;
      editor.renderer.drawModel(editor.rendererContext, editorRef.model);
    }
    triggerCallbacks(editor, undefined, ...types);
  }
}

/**
 * Recognizer callback
 * @param {Editor} editor
 * @param {Object} error
 * @param {Model} model
 * @param {...String} events
 */
function updateCallback(editor, error, model, ...events) {
  const editorRef = editor;

  const handleResult = (err, res, ...types) => {
    if (!err) {
      manageRecognizedModel(
        editorRef,
        res,
        ...[...events, ...types].filter((el, i, a) => i === a.indexOf(el))
      ); // Remove duplicate events
    }
  };
  if (editor.undoRedoManager.updateModel && !error) {
    editor.undoRedoManager.updateModel(
      editor.undoRedoContext,
      model,
      handleResult
    );
  } else {
    handleResult(error, model, ...events);
  }
}

/**
 * Editor
 */
class Editor {
  /**
   * @param {Element} element DOM element to attach this editor
   * @param {Configuration} [configuration] Configuration to apply
   * @param {Theme} [theme] Custom theme to apply
   * @param {PenStyle} [penStyle] Custom style to apply
   * @param {Behaviors} [behaviors] Custom behaviors to apply
   */
  constructor(element, configuration, theme, penStyle) {
    /**
     * Inner reference to the DOM Element
     * @type {Element}
     */
    element.classList.add('mw-editor');
    element.style.position = 'relative';
    element.style.touchAction = 'none';
    // element.style.height = '100%';
    this.domElement = element;

    /**
     * @private
     * @type {Behaviors}
     */
    this.innerBehaviors = overrideDefaultBehaviors({});
    this.configuration = configuration;

    /**
     * Pen color used only for pending stroke
     * @type {string}
     */
    this.localTheme = '';
    this.theme = theme;
    this.penStyle = penStyle;

    this.ticking = false;
    this.domElement.editor = this;

    const eraserElement = document.createElement('img');
    eraserElement.style.position = 'absolute';
    eraserElement.style.display = 'none';
    eraserElement.style.pointerEvents = 'none';
    eraserElement.style.transform = 'translate(-50%, -50%)';
    eraserElement.style.zIndex = '100';
    element.appendChild(eraserElement);
    this.eraserElement = eraserElement;
  }

  /**
   * Set the recognition parameters
   * WARNING : Need to fire a clear if user have already input some strokes.
   * @param {Configuration} configuration
   */
  set configuration(configuration) {
    /**
     * @private
     * @type {Configuration}
     */
    this.innerConfiguration = overrideDefaultConfiguration(
      configuration
    );
    this.behavior = this.behaviors.getBehaviorFromConfiguration(
      this.behaviors,
      this.innerConfiguration
    );
  }

  /**
   * Get the current recognition parameters
   * @return {Configuration}
   */
  get configuration() {
    return this.innerConfiguration
  }

  /**
   * Set the pen style
   * @param {PenStyle} penStyle
   */
  set penStyle(penStyle) {
    if (this.grabber) {
      this.grabber.detach(this.domElement, this.grabberContext);
    }
    if (penStyle.type !== 'select') {
      this.grabberContext = this.grabber.attach(this.domElement, this);
      this.domElement.classList.remove('disabled');
      this.domElement.style.pointerEvents = 'auto';
    } else {
      this.domElement.classList.add('disabled');
      this.domElement.style.pointerEvents = 'none';
    }
    this.innerPenStyle = overrideDefaultPenStyle(penStyle);
    this.localPenStyle = this.innerPenStyle;
  }

  /**
   * Get the pen style
   * @return {PenStyle}
   */
  get penStyle() {
    return this.innerPenStyle
  }

  /**
   * Set the theme
   * @param {Theme} theme
   */
  set theme(theme) {
    /**
     * @private
     * @type {Theme}
     */
    this.innerTheme = overrideDefaultTheme(this.innerTheme, theme);
  }

  /**
   * Get the theme
   * @return {Theme}
   */
  get theme() {
    return this.innerTheme
  }

  /**
   * Get behaviors
   * @return {Behaviors}
   */
  get behaviors() {
    return this.innerBehaviors
  }

  /**
   * @private
   * @param {Behavior} behavior
   */
  set behavior(behavior) {
    if (behavior) {
      if (this.grabber) {
        // Remove event handlers to avoid multiplication (detach grabber)
        this.grabber.detach(this.domElement, this.grabberContext);
      }
      /**
       * @private
       * @type {Behavior}
       */
      this.innerBehavior = behavior;
      this.renderer = this.innerBehavior.renderer;
      /**
       * 动作管理器
       */
      this.undoRedoContext = createUndoRedoContext(
        this.configuration
      );
      this.undoRedoManager = UndoRedoManager;
      this.model = createModel(this.configuration);
      updateCallback(this, undefined, this.model, EventType.RENDERED);

      /**
       * Current grabber context
       * @type {GrabberContext}
       */
      this.grabberContext = this.grabber.attach(this.domElement, this);
    }
  }

  /**
   * Get current behavior
   * @return {Behavior}
   */
  get behavior() {
    return this.innerBehavior
  }

  /**
   * Set the current renderer
   * @private
   * @param {Renderer} renderer
   */
  set renderer(renderer) {
    if (!renderer) return
    if (this.innerRenderer) {
      this.innerRenderer.detach(this.domElement, this.rendererContext);
    }

    /**
     * @private
     * @type {Renderer}
     */
    this.innerRenderer = renderer;

    /**
     * Current rendering context
     * @type {Object}
     */
    this.rendererContext = this.innerRenderer.attach(
      this.domElement,
      this.configuration.renderingParams.minHeight,
      this.configuration.renderingParams.minWidth
    );
  }

  /**
   * Get current renderer
   * @return {Renderer}
   */
  get renderer() {
    return this.innerRenderer
  }

  /**
   * Get current grabber
   * @return {Grabber}
   */
  get grabber() {
    return this.behavior ? this.behavior.grabber : undefined
  }

  /**
   * Get current callbacks
   * @return {Array}
   */
  get callbacks() {
    return this.behavior ? this.behavior.callbacks : undefined
  }

  /**
   * Get a PNG image data url from the data model
   * @return {String}
   */
  get png() {
    return getImage(this.model)
  }

  /**
   * True if initialized, false otherwise
   * @return {Boolean}
   */
  get initialized() {
    return true
  }

  /**
   * Set the rawStrokes
   * @private
   * @param {[Stroke]} strokes
   */
  set strokes(strokes) {
    this.model.rawStrokes = strokes;
  }

  updateEraser(point) {
    this.eraserElement.style.top = point.y + 'px';
    this.eraserElement.style.left = point.x + 'px';
  }

  /**
   * Handle a pointer down
   * @param {{x: Number, y: Number, t: Number}} point Captured point coordinates
   */
  pointerDown(point) {
    let style;
    switch (this.innerPenStyle.type) {
      case 'eraser':
      case 'wiper':
        style = this.theme.eraser;
        const width = style.pointerWidth || style.penWidth || 20;
        const height = style.pointerHeight || Math.round(width * 1.25);
        this.eraserElement.style.display = 'block';
        this.eraserElement.src = style.pointer || ''; // 最好给一个默认图片
        this.eraserElement.style.width = width + 'px';
        this.eraserElement.style.height = height + 'px';
        this.updateEraser(point);
        break
      default:
        clearTimeout(this.renderTimer);
        style = this.theme.ink;
    }
    this.drawByPen = !!point.p;
    this.model = initPendingStroke(
      this.model,
      point,
      Object.assign(style, this.localPenStyle)
    );
    this.renderer.drawCurrentSymbol(this.rendererContext, this.model, {
      drawByPen: this.drawByPen,
      drawStart: true
    });
    triggerCallbacks(this, point, 'pointer-down');
  }

  /**
   * Handle a pointer move
   * @param {{x: Number, y: Number, t: Number}} point Captured point coordinates
   */
  pointerMove(point) {
    const editor = this;
    const drawByPen = this.drawByPen;
    const beginLength = this.model.currentStroke.x.length;
    this.model = appendToPendingStroke(this.model, point);
    const endLength = this.model.currentStroke.x.length;
    triggerCallbacks(this, point, 'pointer-move');

    if (
      this.innerPenStyle.type === 'eraser' ||
      this.innerPenStyle.type === 'wiper'
    ) {
      this.updateEraser(point);
    }

    if (endLength === beginLength) return

    if (!this.ticking) {
      requestAnimFrame(function() {
        editor.renderer.drawCurrentSymbol(
          editor.rendererContext,
          editor.model,
          { drawByPen }
        );
        editor.ticking = false;
      });
      this.ticking = true;
    }
  }

  /**
   * Handle a pointer up
   * @param {{x: Number, y: Number, t: Number}} point Captured point coordinates
   */
  pointerUp(point) {
    this.renderer.drawCurrentSymbol(this.rendererContext, this.model, {
      drawFinal: true,
      drawByPen: this.drawByPen
    });
    this.model = endPendingStroke(
      this.model,
      this.rendererContext.renderingCanvasContext
    );
    triggerCallbacks(this, point, 'pointer-up');

    if (
      this.innerPenStyle.type === 'eraser' ||
      this.innerPenStyle.type === 'wiper'
    ) {
      this.eraserElement.style.display = 'none';
    }

    clearTimeout(this.renderTimer);
    if (this.shouldDelayUpdate()) {
      this.renderTimer = setTimeout(() => {
        this.model.currentStrokes = [];
        updateCallback(this, undefined, this.model);
      }, 1000);
    } else {
      updateCallback(this, undefined, this.model);
    }
  }

  shouldDelayUpdate() {
    const type = this.innerPenStyle.type;
    return type === 'pen' || type === 'pencil' || type === 'brush'
  }

  forceUpdate() {
    clearTimeout(this.renderTimer);
    this.model.currentStrokes = [];
    updateCallback(this, undefined, this.model);
  }

  removeStroke(stroke) {
    const stringRawStrokes = this.model.rawStrokes.map(strokes =>
      JSON.stringify(strokes)
    );
    const strokeIndex = stringRawStrokes.indexOf(JSON.stringify(stroke));
    if (strokeIndex !== -1) {
      this.model.rawStrokes.splice(strokeIndex, 1);
    }
    updateCallback(this, undefined, this.model);
  }

  /**
   * @Deprecated
   * @param rawStrokes
   * @param strokeGroups
   */
  reDraw(rawStrokes = []) {
    this.clear();
    rawStrokes.forEach(stroke => {
      addStroke(
        this.rendererContext.renderingCanvasContext,
        this.model,
        stroke
      );
    });
    updateCallback(this, undefined, this.model);
  }

  /**
   * True if can undo, false otherwise.
   * @return {Boolean}
   */
  get canUndo() {
    return this.undoRedoContext.canUndo
  }

  /**
   * Undo the last action.
   */
  undo() {
    triggerCallbacks(this, undefined, EventType.UNDO);
    this.undoRedoManager.undo(
      this.undoRedoContext,
      this.model,
      (err, res, ...types) => {
        manageRecognizedModel(this, res, ...types);
      }
    );
  }

  /**
   * True if can redo, false otherwise.
   * @return {Boolean}
   */
  get canRedo() {
    return this.undoRedoContext.canRedo
  }

  /**
   * Redo the last action.
   */
  redo() {
    triggerCallbacks(this, undefined, EventType.REDO);
    this.undoRedoManager.redo(
      this.undoRedoContext,
      this.model,
      (err, res, ...types) => {
        manageRecognizedModel(this, res, ...types);
      }
    );
  }

  /**
   * True if empty, false otherwise
   * @returns {boolean}
   */
  get isEmpty() {
    return false
    // return this.recognizerContext.isEmpty
  }

  /**
   * True if can clear, false otherwise.
   * @return {Boolean}
   */
  get canClear() {
    return this.canUndo && this.model.rawStrokes.length > 0
  }

  /**
   * Clear the output and the recognition result.
   */
  clear() {
    const modelRef = clearModel(this.model);
    triggerCallbacks(this, undefined, EventType.CLEAR);
    this.undoRedoContext = resetUndoRedoState(
      this.undoRedoContext
    );
    updateCallback(this, undefined, modelRef, EventType.RENDERED);
  }

  /**
   * Function to call when the dom element link to the current ink paper has been resize.
   */
  resize() {
    this.renderer.resize(
      this.rendererContext,
      this.model,
      this.configuration.renderingParams.minHeight,
      this.configuration.renderingParams.minWidth
    );
  }

  /**
   * Detach event listeners from the DOM element created at editor creation.
   */
  unload() {
    if (this.grabber) {
      this.grabber.detach(this.domElement, this.grabberContext);
    }
    if (this.innerRenderer) {
      this.innerRenderer.detach(this.domElement, this.rendererContext);
    }
  }

  scale(scale, center, { x, y }, { start, end }) {
    this.model.rawStrokes.forEach(s => {
      if (start) {
        s._s = s.s;
        s._center = {
          x: s.center.x,
          y: s.center.y
        };
        s._delta = {
          x: s.delta.x,
          y: s.delta.y
        };
        return
      }
      if (end) {
        delete s._s;
        delete s._center;
        delete s._delta;
      }
      if (s._center && s._s) {
        s.s = s._s * scale;
        s.delta = {
          x: scale * s._delta.x + x,
          y: scale * s._delta.y + y
        };
        if (s.s === 1) {
          s.center.x = s._center.x;
          s.center.y = s._center.y;
        } else {
          s.center.x =
            (center.x * (1 - scale) + s._center.x * (scale - s.s)) / (1 - s.s);
          s.center.y =
            (center.y * (1 - scale) + s._center.y * (scale - s.s)) / (1 - s.s);
        }
      }
    });
    this.renderer.drawModel(this.rendererContext, this.model);
  }
}

/**
 * Attach an Editor to a DOMElement
 * @param {Element} element DOM element to attach an editor
 * @param {Configuration} [configuration] Configuration to apply
 * @param {PenStyle} [penStyle] Pen style to apply
 * @param {Theme} [theme] Theme to apply
 * @param {Behaviors} [behaviors] Custom behaviors to apply
 * @return {Editor} New editor
 */
function register(element, configuration, penStyle, theme, behaviors) {
  return new Editor(element, configuration, penStyle, theme, behaviors)
}

const MyWriting = {
  Constants,
  // Default instantiations
  DefaultConfiguration: defaultConfiguration,
  DefaultBehaviors: defaultBehaviors,
  DefaultPenStyle: defaultPenStyle,
  DefaultTheme: defaultTheme,
  // Helper functions
  register,
  Editor,
  InkModel
};

export default MyWriting;
export { Constants, defaultConfiguration as DefaultConfiguration, defaultBehaviors as DefaultBehaviors, defaultPenStyle as DefaultPenStyle, defaultTheme as DefaultTheme, register, Editor, InkModel };
